001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Florent Guillaume 016 */ 017package org.nuxeo.ecm.core.storage; 018 019import static java.lang.Boolean.FALSE; 020import static java.lang.Boolean.TRUE; 021 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Calendar; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Set; 029import java.util.regex.Pattern; 030 031import org.apache.commons.lang.CharUtils; 032import org.nuxeo.ecm.core.query.QueryParseException; 033import org.nuxeo.ecm.core.query.sql.NXQL; 034import org.nuxeo.ecm.core.query.sql.model.BooleanLiteral; 035import org.nuxeo.ecm.core.query.sql.model.DateLiteral; 036import org.nuxeo.ecm.core.query.sql.model.DoubleLiteral; 037import org.nuxeo.ecm.core.query.sql.model.Expression; 038import org.nuxeo.ecm.core.query.sql.model.Function; 039import org.nuxeo.ecm.core.query.sql.model.IntegerLiteral; 040import org.nuxeo.ecm.core.query.sql.model.Literal; 041import org.nuxeo.ecm.core.query.sql.model.LiteralList; 042import org.nuxeo.ecm.core.query.sql.model.MultiExpression; 043import org.nuxeo.ecm.core.query.sql.model.Operand; 044import org.nuxeo.ecm.core.query.sql.model.Operator; 045import org.nuxeo.ecm.core.query.sql.model.Predicate; 046import org.nuxeo.ecm.core.query.sql.model.Reference; 047import org.nuxeo.ecm.core.query.sql.model.StringLiteral; 048 049/** 050 * Evaluator for an {@link Expression}. 051 * 052 * @since 5.9.4 053 */ 054public abstract class ExpressionEvaluator { 055 056 /** pseudo NXQL to resolve ancestor ids. */ 057 public static final String NXQL_ECM_ANCESTOR_IDS = "ecm:__ancestorIds"; 058 059 /** pseudo NXQL to resolve internal path. */ 060 public static final String NXQL_ECM_PATH = "ecm:__path"; 061 062 /** pseudo NXQL to resolve read acls. */ 063 public static final String NXQL_ECM_READ_ACL = "ecm:__read_acl"; 064 065 /** 066 * Interface for a class that knows how to resolve a path into an id. 067 */ 068 public interface PathResolver { 069 /** 070 * Returns the id for a given path. 071 * 072 * @param path the path 073 * @return the id, or {@code null} if not found 074 */ 075 String getIdForPath(String path); 076 } 077 078 public final PathResolver pathResolver; 079 080 public final Set<String> principals; 081 082 public ExpressionEvaluator(PathResolver pathResolver, String[] principals) { 083 this.pathResolver = pathResolver; 084 this.principals = principals == null ? null : new HashSet<String>(Arrays.asList(principals)); 085 } 086 087 public Object walkExpression(Expression expr) { 088 Operator op = expr.operator; 089 Operand lvalue = expr.lvalue; 090 Operand rvalue = expr.rvalue; 091 String name = lvalue instanceof Reference ? ((Reference) lvalue).name : null; 092 if (op == Operator.STARTSWITH) { 093 return walkStartsWith(lvalue, rvalue); 094 } else if (NXQL.ECM_PATH.equals(name)) { 095 return walkEcmPath(op, rvalue); 096 } else if (NXQL.ECM_ANCESTORID.equals(name)) { 097 return walkAncestorId(op, rvalue); 098 } else if (op == Operator.SUM) { 099 throw new UnsupportedOperationException("SUM"); 100 } else if (op == Operator.SUB) { 101 throw new UnsupportedOperationException("SUB"); 102 } else if (op == Operator.MUL) { 103 throw new UnsupportedOperationException("MUL"); 104 } else if (op == Operator.DIV) { 105 throw new UnsupportedOperationException("DIV"); 106 } else if (op == Operator.LT) { 107 return walkLt(lvalue, rvalue); 108 } else if (op == Operator.GT) { 109 return walkGt(lvalue, rvalue); 110 } else if (op == Operator.EQ) { 111 return walkEq(lvalue, rvalue); 112 } else if (op == Operator.NOTEQ) { 113 return walkNotEq(lvalue, rvalue); 114 } else if (op == Operator.LTEQ) { 115 return walkLtEq(lvalue, rvalue); 116 } else if (op == Operator.GTEQ) { 117 return walkGtEq(lvalue, rvalue); 118 } else if (op == Operator.AND) { 119 if (expr instanceof MultiExpression) { 120 return walkMultiExpression((MultiExpression) expr); 121 } else { 122 return walkAnd(lvalue, rvalue); 123 } 124 } else if (op == Operator.NOT) { 125 return walkNot(lvalue); 126 } else if (op == Operator.OR) { 127 return walkOr(lvalue, rvalue); 128 } else if (op == Operator.LIKE) { 129 return walkLike(lvalue, rvalue, true, false); 130 } else if (op == Operator.ILIKE) { 131 return walkLike(lvalue, rvalue, true, true); 132 } else if (op == Operator.NOTLIKE) { 133 return walkLike(lvalue, rvalue, false, false); 134 } else if (op == Operator.NOTILIKE) { 135 return walkLike(lvalue, rvalue, false, true); 136 } else if (op == Operator.IN) { 137 return walkIn(lvalue, rvalue, true); 138 } else if (op == Operator.NOTIN) { 139 return walkIn(lvalue, rvalue, false); 140 } else if (op == Operator.ISNULL) { 141 return walkIsNull(lvalue); 142 } else if (op == Operator.ISNOTNULL) { 143 return walkIsNotNull(lvalue); 144 } else if (op == Operator.BETWEEN) { 145 return walkBetween(lvalue, rvalue, true); 146 } else if (op == Operator.NOTBETWEEN) { 147 return walkBetween(lvalue, rvalue, false); 148 } else { 149 throw new QueryParseException("Unknown operator: " + op); 150 } 151 } 152 153 protected Boolean walkEcmPath(Operator op, Operand rvalue) { 154 if (op != Operator.EQ && op != Operator.NOTEQ) { 155 throw new QueryParseException(NXQL.ECM_PATH + " requires = or <> operator"); 156 } 157 if (!(rvalue instanceof StringLiteral)) { 158 throw new QueryParseException(NXQL.ECM_PATH + " requires literal path as right argument"); 159 } 160 String path = ((StringLiteral) rvalue).value; 161 if (path.length() > 1 && path.endsWith("/")) { 162 path = path.substring(0, path.length() - 1); 163 } 164 String id = pathResolver.getIdForPath(path); 165 if (id == null) { 166 return FALSE; 167 } 168 Boolean eq = eq(id, walkReference(new Reference(NXQL.ECM_UUID))); 169 return op == Operator.EQ ? eq : not(eq); 170 } 171 172 protected Boolean walkAncestorId(Operator op, Operand rvalue) { 173 if (op != Operator.EQ && op != Operator.NOTEQ) { 174 throw new QueryParseException(NXQL.ECM_ANCESTORID + " requires = or <> operator"); 175 } 176 if (!(rvalue instanceof StringLiteral)) { 177 throw new QueryParseException(NXQL.ECM_ANCESTORID + " requires literal id as right argument"); 178 } 179 String ancestorId = ((StringLiteral) rvalue).value; 180 Object[] ancestorIds = (Object[]) walkReference(new Reference(NXQL_ECM_ANCESTOR_IDS)); 181 boolean eq = op == Operator.EQ ? true : false; 182 if (ancestorIds == null) { 183 // placeless 184 return eq ? FALSE : TRUE; 185 } 186 for (Object id : ancestorIds) { 187 if (ancestorId.equals(id)) { 188 return eq ? TRUE : FALSE; 189 } 190 } 191 return eq ? FALSE : TRUE; 192 } 193 194 public Boolean walkNot(Operand value) { 195 return not(bool(walkOperand(value))); 196 } 197 198 public Boolean walkIsNull(Operand value) { 199 return Boolean.valueOf(walkOperand(value) == null); 200 } 201 202 public Boolean walkIsNotNull(Operand value) { 203 return Boolean.valueOf(walkOperand(value) != null); 204 } 205 206 public Boolean walkMultiExpression(MultiExpression expr) { 207 Boolean res = TRUE; 208 for (Operand value : expr.values) { 209 Boolean bool = bool(walkOperand(value)); 210 if (bool == null) { 211 // null is absorbent 212 return null; 213 } 214 res = and(res, bool); 215 } 216 return res; 217 } 218 219 public Boolean walkAnd(Operand lvalue, Operand rvalue) { 220 Boolean left = bool(walkOperand(lvalue)); 221 Boolean right = bool(walkOperand(rvalue)); 222 return and(left, right); 223 } 224 225 public Boolean walkOr(Operand lvalue, Operand rvalue) { 226 Boolean left = bool(walkOperand(lvalue)); 227 Boolean right = bool(walkOperand(rvalue)); 228 return or(left, right); 229 } 230 231 public Boolean walkEq(Operand lvalue, Operand rvalue) { 232 Object right = walkOperand(rvalue); 233 if (isMixinTypes(lvalue)) { 234 if (!(right instanceof String)) { 235 throw new QueryParseException("Invalid EQ rhs: " + rvalue); 236 } 237 return walkMixinTypes(Collections.singletonList((String) right), true); 238 } 239 Object left = walkOperand(lvalue); 240 return eqMaybeList(left, right); 241 } 242 243 public Boolean walkNotEq(Operand lvalue, Operand rvalue) { 244 if (isMixinTypes(lvalue)) { 245 Object right = walkOperand(rvalue); 246 if (!(right instanceof String)) { 247 throw new QueryParseException("Invalid NE rhs: " + rvalue); 248 } 249 return walkMixinTypes(Collections.singletonList((String) right), false); 250 } 251 return not(walkEq(lvalue, rvalue)); 252 } 253 254 public Boolean walkLt(Operand lvalue, Operand rvalue) { 255 Integer cmp = cmp(lvalue, rvalue); 256 return cmp == null ? null : cmp < 0; 257 } 258 259 public Boolean walkGt(Operand lvalue, Operand rvalue) { 260 Integer cmp = cmp(lvalue, rvalue); 261 return cmp == null ? null : cmp > 0; 262 } 263 264 public Boolean walkLtEq(Operand lvalue, Operand rvalue) { 265 Integer cmp = cmp(lvalue, rvalue); 266 return cmp == null ? null : cmp <= 0; 267 } 268 269 public Boolean walkGtEq(Operand lvalue, Operand rvalue) { 270 Integer cmp = cmp(lvalue, rvalue); 271 return cmp == null ? null : cmp >= 0; 272 } 273 274 public Object walkBetween(Operand lvalue, Operand rvalue, boolean positive) { 275 LiteralList l = (LiteralList) rvalue; 276 Predicate va = new Predicate(lvalue, Operator.GTEQ, l.get(0)); 277 Predicate vb = new Predicate(lvalue, Operator.LTEQ, l.get(1)); 278 Predicate pred = new Predicate(va, Operator.AND, vb); 279 if (!positive) { 280 pred = new Predicate(pred, Operator.NOT, null); 281 } 282 return walkExpression(pred); 283 } 284 285 public Boolean walkIn(Operand lvalue, Operand rvalue, boolean positive) { 286 Object right = walkOperand(rvalue); 287 if (!(right instanceof List)) { 288 throw new QueryParseException("Invalid IN rhs: " + rvalue); 289 } 290 if (isMixinTypes(lvalue)) { 291 return walkMixinTypes((List<String>) right, positive); 292 } 293 Object left = walkOperand(lvalue); 294 Boolean in = inMaybeList(left, (List<Object>) right); 295 return positive ? in : not(in); 296 } 297 298 public Object walkOperand(Operand op) { 299 if (op instanceof Literal) { 300 return walkLiteral((Literal) op); 301 } else if (op instanceof LiteralList) { 302 return walkLiteralList((LiteralList) op); 303 } else if (op instanceof Function) { 304 return walkFunction((Function) op); 305 } else if (op instanceof Expression) { 306 return walkExpression((Expression) op); 307 } else if (op instanceof Reference) { 308 return walkReference((Reference) op); 309 } else { 310 throw new QueryParseException("Unknown operand: " + op); 311 } 312 } 313 314 public Object walkLiteral(Literal lit) { 315 if (lit instanceof BooleanLiteral) { 316 return walkBooleanLiteral((BooleanLiteral) lit); 317 } else if (lit instanceof DateLiteral) { 318 return walkDateLiteral((DateLiteral) lit); 319 } else if (lit instanceof DoubleLiteral) { 320 return walkDoubleLiteral((DoubleLiteral) lit); 321 } else if (lit instanceof IntegerLiteral) { 322 return walkIntegerLiteral((IntegerLiteral) lit); 323 } else if (lit instanceof StringLiteral) { 324 return walkStringLiteral((StringLiteral) lit); 325 } else { 326 throw new QueryParseException("Unknown literal: " + lit); 327 } 328 } 329 330 public Boolean walkBooleanLiteral(BooleanLiteral lit) { 331 return Boolean.valueOf(lit.value); 332 } 333 334 public Calendar walkDateLiteral(DateLiteral lit) { 335 return lit.toCalendar(); // TODO onlyDate 336 } 337 338 public Double walkDoubleLiteral(DoubleLiteral lit) { 339 return Double.valueOf(lit.value); 340 } 341 342 public Long walkIntegerLiteral(IntegerLiteral lit) { 343 return Long.valueOf(lit.value); 344 } 345 346 public String walkStringLiteral(StringLiteral lit) { 347 return lit.value; 348 } 349 350 public List<Object> walkLiteralList(LiteralList litList) { 351 List<Object> list = new ArrayList<Object>(litList.size()); 352 for (Literal lit : litList) { 353 list.add(walkLiteral(lit)); 354 } 355 return list; 356 } 357 358 public Boolean walkLike(Operand lvalue, Operand rvalue, boolean positive, boolean caseInsensitive) { 359 Object left = walkOperand(lvalue); 360 Object right = walkOperand(rvalue); 361 if (!(right instanceof String)) { 362 throw new QueryParseException("Invalid LIKE rhs: " + rvalue); 363 } 364 return likeMaybeList(left, (String) right, positive, caseInsensitive); 365 } 366 367 public Object walkFunction(Function func) { 368 throw new UnsupportedOperationException("Function"); 369 } 370 371 public Boolean walkStartsWith(Operand lvalue, Operand rvalue) { 372 if (!(lvalue instanceof Reference)) { 373 throw new QueryParseException("Invalid STARTSWITH query, left hand side must be a property: " + lvalue); 374 } 375 String name = ((Reference) lvalue).name; 376 if (!(rvalue instanceof StringLiteral)) { 377 throw new QueryParseException("Invalid STARTSWITH query, right hand side must be a literal path: " + rvalue); 378 } 379 String path = ((StringLiteral) rvalue).value; 380 if (path.length() > 1 && path.endsWith("/")) { 381 path = path.substring(0, path.length() - 1); 382 } 383 384 if (NXQL.ECM_PATH.equals(name)) { 385 return walkStartsWithPath(path); 386 } else { 387 return walkStartsWithNonPath(lvalue, path); 388 } 389 } 390 391 protected Boolean walkStartsWithPath(String path) { 392 // resolve path 393 String ancestorId = pathResolver.getIdForPath(path); 394 if (ancestorId == null) { 395 // no such path 396 return FALSE; 397 } 398 Object[] ancestorIds = (Object[]) walkReference(new Reference(NXQL_ECM_ANCESTOR_IDS)); 399 if (ancestorIds == null) { 400 // placeless 401 return FALSE; 402 } 403 for (Object id : ancestorIds) { 404 if (ancestorId.equals(id)) { 405 return TRUE; 406 } 407 } 408 return FALSE; 409 } 410 411 protected Boolean walkStartsWithNonPath(Operand lvalue, String path) { 412 Object left = walkReference((Reference) lvalue); 413 // exact match 414 Boolean bool = eqMaybeList(left, path); 415 if (TRUE.equals(bool)) { 416 return TRUE; 417 } 418 // prefix match TODO escape % chars 419 String pattern = path + "/%"; 420 return likeMaybeList(left, pattern, true, false); 421 } 422 423 /** 424 * Evaluates a reference over the context state. 425 * 426 * @param ref the reference 427 */ 428 public abstract Object walkReference(Reference ref); 429 430 protected boolean isMixinTypes(Operand op) { 431 if (!(op instanceof Reference)) { 432 return false; 433 } 434 return ((Reference) op).name.equals(NXQL.ECM_MIXINTYPE); 435 } 436 437 protected Boolean bool(Object value) { 438 if (value == null) { 439 return null; 440 } 441 if (!(value instanceof Boolean)) { 442 throw new QueryParseException("Not a boolean: " + value); 443 } 444 return (Boolean) value; 445 } 446 447 // ternary logic 448 protected Boolean not(Boolean value) { 449 if (value == null) { 450 return null; 451 } 452 return !value; 453 } 454 455 // ternary logic 456 protected Boolean and(Boolean left, Boolean right) { 457 if (TRUE.equals(left)) { 458 return right; 459 } else { 460 return left; 461 } 462 } 463 464 // ternary logic 465 protected Boolean or(Boolean left, Boolean right) { 466 if (TRUE.equals(left)) { 467 return left; 468 } else { 469 return right; 470 } 471 } 472 473 // ternary logic 474 protected Boolean eq(Object left, Object right) { 475 if (left == null || right == null) { 476 return null; 477 } 478 return left.equals(right); 479 } 480 481 // ternary logic 482 protected Boolean in(Object left, List<Object> right) { 483 if (left == null) { 484 return null; 485 } 486 boolean hasNull = false; 487 for (Object r : right) { 488 if (r == null) { 489 hasNull = true; 490 } else if (left.equals(r)) { 491 return TRUE; 492 } 493 } 494 return hasNull ? null : FALSE; 495 } 496 497 protected Integer cmp(Operand lvalue, Operand rvalue) { 498 Object left = walkOperand(lvalue); 499 Object right = walkOperand(rvalue); 500 return cmp(left, right); 501 } 502 503 // ternary logic 504 protected Integer cmp(Object left, Object right) { 505 if (left == null || right == null) { 506 return null; 507 } 508 if (!(left instanceof Comparable)) { 509 throw new QueryParseException("Not a comparable: " + left); 510 } 511 return ((Comparable<Object>) left).compareTo(right); 512 } 513 514 // ternary logic 515 protected Boolean like(Object left, String right, boolean caseInsensitive) { 516 if (left == null || right == null) { 517 return null; 518 } 519 if (!(left instanceof String)) { 520 throw new QueryParseException("Invalid LIKE lhs: " + left); 521 } 522 String value = (String) left; 523 if (caseInsensitive) { 524 value = value.toLowerCase(); 525 right = right.toLowerCase(); 526 } 527 String regex = likeToRegex(right); 528 boolean match = Pattern.matches(regex.toString(), value); 529 return match; 530 } 531 532 /** 533 * Turns a NXQL LIKE pattern into a regex. 534 * <p> 535 * % and _ are standard wildcards, and \ escapes them. 536 * 537 * @since 7.4 538 */ 539 public static String likeToRegex(String like) { 540 StringBuilder regex = new StringBuilder(); 541 char[] chars = like.toCharArray(); 542 boolean escape = false; 543 for (int i = 0; i < chars.length; i++) { 544 char c = chars[i]; 545 boolean escapeNext = false; 546 switch (c) { 547 case '%': 548 if (escape) { 549 regex.append(c); 550 } else { 551 regex.append(".*"); 552 } 553 break; 554 case '_': 555 if (escape) { 556 regex.append(c); 557 } else { 558 regex.append("."); 559 } 560 break; 561 case '\\': 562 if (escape) { 563 regex.append("\\\\"); // backslash escaped for regexp 564 } else { 565 escapeNext = true; 566 } 567 break; 568 default: 569 // escape mostly everything just in case 570 if (!CharUtils.isAsciiAlphanumeric(c)) { 571 regex.append("\\"); 572 } 573 regex.append(c); 574 break; 575 } 576 escape = escapeNext; 577 } 578 if (escape) { 579 // invalid string terminated by escape character, ignore 580 } 581 return regex.toString(); 582 } 583 584 // if list, use EXIST (SELECT 1 FROM left WHERE left.item = right) 585 protected Boolean eqMaybeList(Object left, Object right) { 586 if (left instanceof Object[]) { 587 for (Object l : ((Object[]) left)) { 588 Boolean eq = eq(l, right); 589 if (TRUE.equals(eq)) { 590 return TRUE; 591 } 592 } 593 return FALSE; 594 } else { 595 return eq(left, right); 596 } 597 } 598 599 // if list, use EXIST (SELECT 1 FROM left WHERE left.item IN right) 600 protected Boolean inMaybeList(Object left, List<Object> right) { 601 if (left instanceof Object[]) { 602 for (Object l : ((Object[]) left)) { 603 Boolean in = in(l, right); 604 if (TRUE.equals(in)) { 605 return TRUE; 606 } 607 } 608 return FALSE; 609 } else { 610 return in(left, right); 611 } 612 } 613 614 protected Boolean likeMaybeList(Object left, String right, boolean positive, boolean caseInsensitive) { 615 if (left instanceof Object[]) { 616 for (Object l : ((Object[]) left)) { 617 Boolean like = like(l, right, caseInsensitive); 618 if (TRUE.equals(like)) { 619 return Boolean.valueOf(positive); 620 } 621 } 622 return Boolean.valueOf(!positive); 623 } else { 624 Boolean like = like(left, right, caseInsensitive); 625 return positive ? like : not(like); 626 } 627 } 628 629 /** 630 * Matches the mixin types against a list of values. 631 * <p> 632 * Used for: 633 * <ul> 634 * <li>ecm:mixinTypes = 'foo' 635 * <li>ecm:mixinTypes != 'foo' 636 * <li>ecm:mixinTypes IN ('foo', 'bar') 637 * <li>ecm:mixinTypes NOT IN ('foo', 'bar') 638 * </ul> 639 * 640 * @param mixins the mixin(s) to match 641 * @param include {@code true} for = and IN 642 * @since 7.4 643 */ 644 public abstract Boolean walkMixinTypes(List<String> mixins, boolean include); 645 646}