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 */
019
020package org.nuxeo.ecm.directory.ldap;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.List;
026
027import org.nuxeo.ecm.core.query.QueryParseException;
028import org.nuxeo.ecm.core.query.sql.model.BooleanLiteral;
029import org.nuxeo.ecm.core.query.sql.model.DateLiteral;
030import org.nuxeo.ecm.core.query.sql.model.DoubleLiteral;
031import org.nuxeo.ecm.core.query.sql.model.Expression;
032import org.nuxeo.ecm.core.query.sql.model.Function;
033import org.nuxeo.ecm.core.query.sql.model.IntegerLiteral;
034import org.nuxeo.ecm.core.query.sql.model.Literal;
035import org.nuxeo.ecm.core.query.sql.model.LiteralList;
036import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
037import org.nuxeo.ecm.core.query.sql.model.Operand;
038import org.nuxeo.ecm.core.query.sql.model.Operator;
039import org.nuxeo.ecm.core.query.sql.model.Predicate;
040import org.nuxeo.ecm.core.query.sql.model.Reference;
041import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
042import org.nuxeo.ecm.core.schema.types.Field;
043import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
044
045/**
046 * Creates an LDAP query filter from a Nuxeo Expression.
047 *
048 * @since 10.3
049 */
050public class LDAPFilterBuilder {
051
052    protected static final String DATE_CAST = "DATE";
053
054    protected final LDAPDirectory directory;
055
056    public StringBuilder filter = new StringBuilder();
057
058    public int paramIndex = 0;
059
060    public final List<Serializable> params = new ArrayList<>();
061
062    public LDAPFilterBuilder(LDAPDirectory directory) {
063        this.directory = directory;
064    }
065
066    public void walk(Expression expression) {
067        if (expression instanceof MultiExpression && ((MultiExpression) expression).predicates.isEmpty()) {
068            // special-case empty query
069            return;
070        } else {
071            walkExpression(expression);
072        }
073    }
074
075    public void walkExpression(Expression expr) {
076        Operator op = expr.operator;
077        Operand lvalue = expr.lvalue;
078        Operand rvalue = expr.rvalue;
079        Reference ref = lvalue instanceof Reference ? (Reference) lvalue : null;
080        String name = ref != null ? ref.name : null;
081        String cast = ref != null ? ref.cast : null;
082        if (DATE_CAST.equals(cast)) {
083            checkDateLiteralForCast(op, rvalue, name);
084        }
085        if (op == Operator.SUM) {
086            throw new QueryParseException("SUM");
087        } else if (op == Operator.SUB) {
088            throw new QueryParseException("SUB");
089        } else if (op == Operator.MUL) {
090            throw new QueryParseException("MUL");
091        } else if (op == Operator.DIV) {
092            throw new QueryParseException("DIV");
093        } else if (op == Operator.LT) {
094            walkLt(lvalue, rvalue);
095        } else if (op == Operator.GT) {
096            walkGt(lvalue, rvalue);
097        } else if (op == Operator.EQ) {
098            walkEq(lvalue, rvalue);
099        } else if (op == Operator.NOTEQ) {
100            walkNotEq(lvalue, rvalue);
101        } else if (op == Operator.LTEQ) {
102            walkLtEq(lvalue, rvalue);
103        } else if (op == Operator.GTEQ) {
104            walkGtEq(lvalue, rvalue);
105        } else if (op == Operator.AND) {
106            if (expr instanceof MultiExpression) {
107                walkAndMultiExpression((MultiExpression) expr);
108            } else {
109                walkAnd(expr);
110            }
111        } else if (op == Operator.NOT) {
112            walkNot(lvalue);
113        } else if (op == Operator.OR) {
114            if (expr instanceof MultiExpression) {
115                walkOrMultiExpression((MultiExpression) expr);
116            } else {
117                walkOr(expr);
118            }
119        } else if (op == Operator.LIKE) {
120            walkLike(lvalue, rvalue, true, false);
121        } else if (op == Operator.ILIKE) {
122            walkLike(lvalue, rvalue, true, true);
123        } else if (op == Operator.NOTLIKE) {
124            walkLike(lvalue, rvalue, false, false);
125        } else if (op == Operator.NOTILIKE) {
126            walkLike(lvalue, rvalue, false, true);
127        } else if (op == Operator.IN) {
128            walkIn(lvalue, rvalue, true);
129        } else if (op == Operator.NOTIN) {
130            walkIn(lvalue, rvalue, false);
131        } else if (op == Operator.ISNULL) {
132            walkIsNull(lvalue);
133        } else if (op == Operator.ISNOTNULL) {
134            walkIsNotNull(lvalue);
135        } else if (op == Operator.BETWEEN) {
136            walkBetween(lvalue, rvalue, true);
137        } else if (op == Operator.NOTBETWEEN) {
138            walkBetween(lvalue, rvalue, false);
139        } else {
140            throw new QueryParseException("Unknown operator: " + op);
141        }
142    }
143
144    protected void checkDateLiteralForCast(Operator op, Operand value, String name) {
145        if (op == Operator.BETWEEN || op == Operator.NOTBETWEEN) {
146            LiteralList l = (LiteralList) value;
147            checkDateLiteralForCast(l.get(0), name);
148            checkDateLiteralForCast(l.get(1), name);
149        } else {
150            checkDateLiteralForCast(value, name);
151        }
152    }
153
154    protected void checkDateLiteralForCast(Operand value, String name) {
155        if (value instanceof DateLiteral && !((DateLiteral) value).onlyDate) {
156            throw new QueryParseException("DATE() cast must be used with DATE literal, not TIMESTAMP: " + name);
157        }
158    }
159
160    public void walkNot(Operand value) {
161        filter.append("(!");
162        walkOperand(value);
163        filter.append(')');
164    }
165
166    public void walkIsNull(Operand value) {
167        filter.append("(!");
168        walkIsNotNull(value);
169        filter.append(')');
170    }
171
172    public void walkIsNotNull(Operand value) {
173        filter.append('(');
174        walkReference(value);
175        filter.append("=*)");
176    }
177
178    public void walkAndMultiExpression(MultiExpression expr) {
179        walkMulti("&", expr.predicates);
180    }
181
182    public void walkAnd(Expression expr) {
183        walkMulti("&", Arrays.asList(expr.lvalue, expr.rvalue));
184    }
185
186    public void walkOrMultiExpression(MultiExpression expr) {
187        walkMulti("|", expr.predicates);
188    }
189
190    public void walkOr(Expression expr) {
191        walkMulti("|", Arrays.asList(expr.lvalue, expr.rvalue));
192    }
193
194    protected void walkMulti(String op, List<? extends Operand> values) {
195        if (values.size() == 1) {
196            walkOperand(values.get(0));
197        } else {
198            filter.append('(');
199            filter.append(op);
200            for (Operand value : values) {
201                walkOperand(value);
202            }
203            filter.append(')');
204        }
205    }
206
207    public void walkEq(Operand lvalue, Operand rvalue) {
208        walkBinOp("=", lvalue, rvalue);
209    }
210
211    public void walkNotEq(Operand lvalue, Operand rvalue) {
212        filter.append("(!");
213        walkEq(lvalue, rvalue);
214        filter.append(')');
215    }
216
217    public void walkLt(Operand lvalue, Operand rvalue) {
218        walkBinOp("<", lvalue, rvalue);
219    }
220
221    public void walkGt(Operand lvalue, Operand rvalue) {
222        walkBinOp(">", lvalue, rvalue);
223    }
224
225    public void walkLtEq(Operand lvalue, Operand rvalue) {
226        walkBinOp("<=", lvalue, rvalue);
227    }
228
229    public void walkGtEq(Operand lvalue, Operand rvalue) {
230        walkBinOp(">=", lvalue, rvalue);
231    }
232
233    protected void walkBinOp(String op, Operand lvalue, Operand rvalue) {
234        filter.append('(');
235        Field field = walkReference(lvalue);
236        filter.append(op);
237        if (field.getType() instanceof BooleanType) {
238            rvalue = makeBoolean(rvalue);
239        }
240        walkLiteral(rvalue);
241        filter.append(')');
242    }
243
244    protected Operand makeBoolean(Operand rvalue) {
245        if (rvalue instanceof BooleanLiteral) {
246            return rvalue;
247        }
248        long v;
249        if (!(rvalue instanceof IntegerLiteral) || ((v = ((IntegerLiteral) rvalue).value) != 0 && v != 1)) {
250            throw new QueryParseException("Boolean expressions require boolean or literal 0 or 1 as right argument");
251        }
252        return new BooleanLiteral(v == 1);
253    }
254
255    public void walkBetween(Operand lvalue, Operand rvalue, boolean positive) {
256        LiteralList list = (LiteralList) rvalue;
257        Literal left = list.get(0);
258        Literal right = list.get(1);
259        if (!positive) {
260            filter.append("(!");
261        }
262        filter.append("(&");
263        walkGtEq(lvalue, left);
264        walkLtEq(lvalue, right);
265        filter.append(')');
266        if (!positive) {
267            filter.append(')');
268        }
269    }
270
271    public void walkIn(Operand lvalue, Operand rvalue, boolean positive) {
272        if (!positive) {
273            filter.append("(!");
274        }
275        filter.append("(|");
276        for (Literal value : (LiteralList) rvalue) {
277            walkEq(lvalue, value);
278        }
279        filter.append(')');
280        if (!positive) {
281            filter.append(')');
282        }
283    }
284
285    public void walkLike(Operand lvalue, Operand rvalue, boolean positive, boolean caseInsensitive) {
286        if (!(rvalue instanceof StringLiteral)) {
287            throw new QueryParseException("Invalid LIKE, right hand side must be a string: " + rvalue);
288        }
289        String like = ((StringLiteral) rvalue).value;
290        if (caseInsensitive) {
291            like = like.toLowerCase();
292        }
293
294        if (!positive) {
295            filter.append("(!");
296        }
297        filter.append('(');
298        walkReference(lvalue);
299        filter.append('=');
300        walkLikeWildcard(like);
301        filter.append(')');
302        if (!positive) {
303            filter.append(')');
304        }
305    }
306
307    /**
308     * Turns a NXQL LIKE pattern into an LDAP wildcard.
309     * <p>
310     * % and _ are standard wildcards, and \ escapes them.
311     */
312    public void walkLikeWildcard(String like) {
313        StringBuilder param = new StringBuilder();
314        char[] chars = like.toCharArray();
315        boolean escape = false;
316        for (int i = 0; i < chars.length; i++) {
317            char c = chars[i];
318            boolean escapeNext = false;
319            if (escape) {
320                param.append(c);
321            } else {
322                switch (c) {
323                case '%':
324                    if (param.length() != 0) {
325                        addFilterParam(param.toString());
326                        param.setLength(0);
327                    }
328                    filter.append('*');
329                    break;
330                case '_':
331                    throw new QueryParseException("Cannot use _ wildcard in LIKE for LDAP directory");
332                case '\\':
333                    escapeNext = true;
334                    break;
335                default:
336                    param.append(c);
337                    break;
338                }
339            }
340            escape = escapeNext;
341        }
342        if (escape) {
343            throw new QueryParseException("Invalid LIKE parameter ending with escape character");
344        }
345        if (param.length() != 0) {
346            addFilterParam(param.toString());
347        }
348    }
349
350    public void walkOperand(Operand operand) {
351        if (operand instanceof Literal) {
352            walkLiteral((Literal) operand);
353        } else if (operand instanceof Function) {
354            walkFunction((Function) operand);
355        } else if (operand instanceof Expression) {
356            walkExpression((Expression) operand);
357        } else if (operand instanceof Reference) {
358            walkReference((Reference) operand);
359        } else {
360            throw new QueryParseException("Unknown operand: " + operand);
361        }
362    }
363
364    public void walkLiteral(Operand operand) {
365        if (!(operand instanceof Literal)) {
366            throw new QueryParseException("Requires literal instead of: " + operand);
367        }
368        Literal lit = (Literal) operand;
369        if (lit instanceof BooleanLiteral) {
370            walkBooleanLiteral((BooleanLiteral) lit);
371        } else if (lit instanceof DateLiteral) {
372            walkDateLiteral((DateLiteral) lit);
373        } else if (lit instanceof DoubleLiteral) {
374            walkDoubleLiteral((DoubleLiteral) lit);
375        } else if (lit instanceof IntegerLiteral) {
376            walkIntegerLiteral((IntegerLiteral) lit);
377        } else if (lit instanceof StringLiteral) {
378            walkStringLiteral((StringLiteral) lit);
379        } else {
380            throw new QueryParseException("Unknown literal: " + lit);
381        }
382    }
383
384    public void walkBooleanLiteral(BooleanLiteral lit) {
385        addFilterParam(Boolean.valueOf(lit.value));
386    }
387
388    public void walkDateLiteral(DateLiteral lit) {
389        if (lit.onlyDate) {
390            throw new QueryParseException("Cannot use only date in LDAP query: " + lit);
391        }
392        addFilterParam(lit.toCalendar()); // let LDAP library serialize it
393    }
394
395    public void walkDoubleLiteral(DoubleLiteral lit) {
396        addFilterParam(Double.valueOf(lit.value));
397    }
398
399    public void walkIntegerLiteral(IntegerLiteral lit) {
400        addFilterParam(Long.valueOf(lit.value));
401    }
402
403    public void walkStringLiteral(StringLiteral lit) {
404        addFilterParam(lit.value);
405    }
406
407    protected void addFilterParam(Serializable value) {
408        filter.append('{');
409        filter.append(paramIndex++);
410        filter.append('}');
411        params.add(value);
412    }
413
414    public Object walkFunction(Function func) {
415        throw new QueryParseException(func.name);
416    }
417
418    public Field walkReference(Operand value) {
419        if (!(value instanceof Reference)) {
420            throw new QueryParseException("Invalid query, left hand side must be a property: " + value);
421        }
422        String name = ((Reference) value).name;
423        if (directory.isReference(name)) {
424            throw new QueryParseException(
425                    "Column: " + name + " is a reference and cannot be queried for directory: " + directory.getName());
426        }
427        Field field = directory.getSchemaFieldMap().get(name);
428        if (field == null) {
429            throw new QueryParseException("No column: " + name + " for directory: " + directory.getName());
430        }
431        String backend = directory.getFieldMapper().getBackendField(name);
432        filter.append(backend);
433        return field;
434    }
435
436}