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}