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.Reference;
040import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
041import org.nuxeo.ecm.core.schema.types.Field;
042import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
043
044/**
045 * Creates an LDAP query filter from a Nuxeo Expression.
046 *
047 * @since 10.3
048 */
049public class LDAPFilterBuilder {
050
051    protected static final String DATE_CAST = "DATE";
052
053    protected final LDAPDirectory directory;
054
055    public StringBuilder filter = new StringBuilder();
056
057    public int paramIndex = 0;
058
059    public final List<Serializable> params = new ArrayList<>();
060
061    public LDAPFilterBuilder(LDAPDirectory directory) {
062        this.directory = directory;
063    }
064
065    public void walk(Expression expression) {
066        if (expression instanceof MultiExpression && ((MultiExpression) expression).predicates.isEmpty()) {
067            // special-case empty query
068            return;
069        } else {
070            walkExpression(expression);
071        }
072    }
073
074    public void walkExpression(Expression expr) {
075        Operator op = expr.operator;
076        Operand lvalue = expr.lvalue;
077        Operand rvalue = expr.rvalue;
078        Reference ref = lvalue instanceof Reference ? (Reference) lvalue : null;
079        String name = ref != null ? ref.name : null;
080        String cast = ref != null ? ref.cast : null;
081        if (DATE_CAST.equals(cast)) {
082            checkDateLiteralForCast(op, rvalue, name);
083        }
084        if (op == Operator.SUM) {
085            throw new QueryParseException("SUM");
086        } else if (op == Operator.SUB) {
087            throw new QueryParseException("SUB");
088        } else if (op == Operator.MUL) {
089            throw new QueryParseException("MUL");
090        } else if (op == Operator.DIV) {
091            throw new QueryParseException("DIV");
092        } else if (op == Operator.LT) {
093            walkLt(lvalue, rvalue);
094        } else if (op == Operator.GT) {
095            walkGt(lvalue, rvalue);
096        } else if (op == Operator.EQ) {
097            walkEq(lvalue, rvalue);
098        } else if (op == Operator.NOTEQ) {
099            walkNotEq(lvalue, rvalue);
100        } else if (op == Operator.LTEQ) {
101            walkLtEq(lvalue, rvalue);
102        } else if (op == Operator.GTEQ) {
103            walkGtEq(lvalue, rvalue);
104        } else if (op == Operator.AND) {
105            if (expr instanceof MultiExpression) {
106                walkAndMultiExpression((MultiExpression) expr);
107            } else {
108                walkAnd(expr);
109            }
110        } else if (op == Operator.NOT) {
111            walkNot(lvalue);
112        } else if (op == Operator.OR) {
113            if (expr instanceof MultiExpression) {
114                walkOrMultiExpression((MultiExpression) expr);
115            } else {
116                walkOr(expr);
117            }
118        } else if (op == Operator.LIKE) {
119            walkLike(lvalue, rvalue, true, false);
120        } else if (op == Operator.ILIKE) {
121            walkLike(lvalue, rvalue, true, true);
122        } else if (op == Operator.NOTLIKE) {
123            walkLike(lvalue, rvalue, false, false);
124        } else if (op == Operator.NOTILIKE) {
125            walkLike(lvalue, rvalue, false, true);
126        } else if (op == Operator.IN) {
127            walkIn(lvalue, rvalue, true);
128        } else if (op == Operator.NOTIN) {
129            walkIn(lvalue, rvalue, false);
130        } else if (op == Operator.ISNULL) {
131            walkIsNull(lvalue);
132        } else if (op == Operator.ISNOTNULL) {
133            walkIsNotNull(lvalue);
134        } else if (op == Operator.BETWEEN) {
135            walkBetween(lvalue, rvalue, true);
136        } else if (op == Operator.NOTBETWEEN) {
137            walkBetween(lvalue, rvalue, false);
138        } else {
139            throw new QueryParseException("Unknown operator: " + op);
140        }
141    }
142
143    protected void checkDateLiteralForCast(Operator op, Operand value, String name) {
144        if (op == Operator.BETWEEN || op == Operator.NOTBETWEEN) {
145            LiteralList l = (LiteralList) value;
146            checkDateLiteralForCast(l.get(0), name);
147            checkDateLiteralForCast(l.get(1), name);
148        } else {
149            checkDateLiteralForCast(value, name);
150        }
151    }
152
153    protected void checkDateLiteralForCast(Operand value, String name) {
154        if (value instanceof DateLiteral && !((DateLiteral) value).onlyDate) {
155            throw new QueryParseException("DATE() cast must be used with DATE literal, not TIMESTAMP: " + name);
156        }
157    }
158
159    public void walkNot(Operand value) {
160        filter.append("(!");
161        walkOperand(value);
162        filter.append(')');
163    }
164
165    public void walkIsNull(Operand value) {
166        filter.append("(!");
167        walkIsNotNull(value);
168        filter.append(')');
169    }
170
171    public void walkIsNotNull(Operand value) {
172        filter.append('(');
173        walkReference(value);
174        filter.append("=*)");
175    }
176
177    public void walkAndMultiExpression(MultiExpression expr) {
178        walkMulti("&", expr.predicates);
179    }
180
181    public void walkAnd(Expression expr) {
182        walkMulti("&", Arrays.asList(expr.lvalue, expr.rvalue));
183    }
184
185    public void walkOrMultiExpression(MultiExpression expr) {
186        walkMulti("|", expr.predicates);
187    }
188
189    public void walkOr(Expression expr) {
190        walkMulti("|", Arrays.asList(expr.lvalue, expr.rvalue));
191    }
192
193    protected void walkMulti(String op, List<? extends Operand> values) {
194        if (values.size() == 1) {
195            walkOperand(values.get(0));
196        } else {
197            filter.append('(');
198            filter.append(op);
199            for (Operand value : values) {
200                walkOperand(value);
201            }
202            filter.append(')');
203        }
204    }
205
206    public void walkEq(Operand lvalue, Operand rvalue) {
207        walkBinOp("=", lvalue, rvalue);
208    }
209
210    public void walkNotEq(Operand lvalue, Operand rvalue) {
211        filter.append("(!");
212        walkEq(lvalue, rvalue);
213        filter.append(')');
214    }
215
216    public void walkLt(Operand lvalue, Operand rvalue) {
217        walkBinOp("<", lvalue, rvalue);
218    }
219
220    public void walkGt(Operand lvalue, Operand rvalue) {
221        walkBinOp(">", lvalue, rvalue);
222    }
223
224    public void walkLtEq(Operand lvalue, Operand rvalue) {
225        walkBinOp("<=", lvalue, rvalue);
226    }
227
228    public void walkGtEq(Operand lvalue, Operand rvalue) {
229        walkBinOp(">=", lvalue, rvalue);
230    }
231
232    protected void walkBinOp(String op, Operand lvalue, Operand rvalue) {
233        filter.append('(');
234        Field field = walkReference(lvalue);
235        filter.append(op);
236        if (field.getType() instanceof BooleanType) {
237            rvalue = makeBoolean(rvalue);
238        }
239        walkLiteral(rvalue);
240        filter.append(')');
241    }
242
243    protected Operand makeBoolean(Operand rvalue) {
244        if (rvalue instanceof BooleanLiteral) {
245            return rvalue;
246        }
247        long v;
248        if (!(rvalue instanceof IntegerLiteral) || ((v = ((IntegerLiteral) rvalue).value) != 0 && v != 1)) {
249            throw new QueryParseException("Boolean expressions require boolean or literal 0 or 1 as right argument");
250        }
251        return new BooleanLiteral(v == 1);
252    }
253
254    public void walkBetween(Operand lvalue, Operand rvalue, boolean positive) {
255        LiteralList list = (LiteralList) rvalue;
256        Literal left = list.get(0);
257        Literal right = list.get(1);
258        if (!positive) {
259            filter.append("(!");
260        }
261        filter.append("(&");
262        walkGtEq(lvalue, left);
263        walkLtEq(lvalue, right);
264        filter.append(')');
265        if (!positive) {
266            filter.append(')');
267        }
268    }
269
270    public void walkIn(Operand lvalue, Operand rvalue, boolean positive) {
271        if (!positive) {
272            filter.append("(!");
273        }
274        filter.append("(|");
275        for (Literal value : (LiteralList) rvalue) {
276            walkEq(lvalue, value);
277        }
278        filter.append(')');
279        if (!positive) {
280            filter.append(')');
281        }
282    }
283
284    public void walkLike(Operand lvalue, Operand rvalue, boolean positive, boolean caseInsensitive) {
285        if (!(rvalue instanceof StringLiteral)) {
286            throw new QueryParseException("Invalid LIKE, right hand side must be a string: " + rvalue);
287        }
288        String like = ((StringLiteral) rvalue).value;
289        if (caseInsensitive) {
290            like = like.toLowerCase();
291        }
292
293        if (!positive) {
294            filter.append("(!");
295        }
296        filter.append('(');
297        walkReference(lvalue);
298        filter.append('=');
299        walkLikeWildcard(like);
300        filter.append(')');
301        if (!positive) {
302            filter.append(')');
303        }
304    }
305
306    /**
307     * Turns a NXQL LIKE pattern into an LDAP wildcard.
308     * <p>
309     * % and _ are standard wildcards, and \ escapes them.
310     */
311    public void walkLikeWildcard(String like) {
312        StringBuilder param = new StringBuilder();
313        char[] chars = like.toCharArray();
314        boolean escape = false;
315        for (int i = 0; i < chars.length; i++) {
316            char c = chars[i];
317            boolean escapeNext = false;
318            if (escape) {
319                param.append(c);
320            } else {
321                switch (c) {
322                case '%':
323                    if (param.length() != 0) {
324                        addFilterParam(param.toString());
325                        param.setLength(0);
326                    }
327                    filter.append('*');
328                    break;
329                case '_': // interpret it as an escaped _, not a wildcard
330                    param.append(c);
331                    break;
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(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(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}