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}