001/* 002 * (C) Copyright 2014-2016 Nuxeo SA (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 */ 019package org.nuxeo.ecm.core.storage.dbs; 020 021import static java.lang.Boolean.FALSE; 022import static java.lang.Boolean.TRUE; 023import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL; 024import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL_NAME; 025import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACP; 026import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID; 027import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MIXIN_TYPES; 028import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME; 029import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID; 030import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PRIMARY_TYPE; 031import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_READ_ACL; 032 033import java.io.Serializable; 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.Calendar; 037import java.util.Collections; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.Iterator; 041import java.util.List; 042import java.util.Map; 043import java.util.Set; 044 045import org.apache.commons.lang.StringUtils; 046import org.apache.commons.lang.math.NumberUtils; 047import org.apache.commons.logging.Log; 048import org.apache.commons.logging.LogFactory; 049import org.nuxeo.ecm.core.query.QueryParseException; 050import org.nuxeo.ecm.core.query.sql.NXQL; 051import org.nuxeo.ecm.core.query.sql.model.Expression; 052import org.nuxeo.ecm.core.query.sql.model.MultiExpression; 053import org.nuxeo.ecm.core.query.sql.model.Operand; 054import org.nuxeo.ecm.core.query.sql.model.OrderByClause; 055import org.nuxeo.ecm.core.query.sql.model.OrderByExpr; 056import org.nuxeo.ecm.core.query.sql.model.Reference; 057import org.nuxeo.ecm.core.query.sql.model.SelectClause; 058import org.nuxeo.ecm.core.query.sql.model.SelectList; 059import org.nuxeo.ecm.core.schema.DocumentType; 060import org.nuxeo.ecm.core.schema.SchemaManager; 061import org.nuxeo.ecm.core.schema.types.ComplexType; 062import org.nuxeo.ecm.core.schema.types.Field; 063import org.nuxeo.ecm.core.schema.types.ListType; 064import org.nuxeo.ecm.core.schema.types.Schema; 065import org.nuxeo.ecm.core.schema.types.Type; 066import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 067import org.nuxeo.ecm.core.schema.types.primitives.DateType; 068import org.nuxeo.ecm.core.storage.ExpressionEvaluator; 069import org.nuxeo.ecm.core.storage.State; 070import org.nuxeo.runtime.api.Framework; 071 072/** 073 * Expression evaluator for a {@link DBSDocument} state. 074 * 075 * @since 5.9.4 076 */ 077public class DBSExpressionEvaluator extends ExpressionEvaluator { 078 079 private static final Log log = LogFactory.getLog(DBSExpressionEvaluator.class); 080 081 private static final Long ZERO = Long.valueOf(0); 082 083 private static final Long ONE = Long.valueOf(1); 084 085 protected final SelectClause selectClause; 086 087 protected final Expression expression; 088 089 protected final OrderByClause orderByClause; 090 091 protected SchemaManager schemaManager; 092 093 protected List<String> documentTypes; 094 095 protected State state; 096 097 protected boolean parsing; 098 099 /** Info about a value and how to compute it from the toplevel state or an iterator's state. */ 100 protected static final class ValueInfo { 101 102 /** 103 * Traversed steps to compute this value from a state. Traversal steps can be: 104 * <ul> 105 * <li>String: a map key. 106 * <li>Integer: a list element. 107 * </ul> 108 */ 109 // also used to temporarily hold the full parsed reference 110 public List<Serializable> steps; 111 112 // original NXQL name in query 113 public final String nxqlProp; 114 115 public final String canonRef; 116 117 public Type type; 118 119 public boolean isTrueOrNullBoolean; 120 121 public boolean isDateCast; 122 123 /** The value computed for this reference. */ 124 public Object value; 125 126 public ValueInfo(List<Serializable> steps, String nxqlProp, String canonRef) { 127 this.steps = steps; 128 this.nxqlProp = nxqlProp; 129 this.canonRef = canonRef; 130 } 131 132 public Object getValueForEvaluation() { 133 if (type instanceof BooleanType) { 134 // boolean evaluation is like 0 / 1 135 if (isTrueOrNullBoolean) { 136 return TRUE.equals(value) ? ONE : ZERO; 137 } else { 138 return value == null ? null : (((Boolean) value).booleanValue() ? ONE : ZERO); 139 } 140 } else if (isDateCast) { 141 if (value == null) { 142 return null; 143 } else if (value instanceof Calendar) { 144 return castToDate((Calendar) value); 145 } else { // array 146 Object[] array = (Object[]) value; 147 List<Calendar> dates = new ArrayList<>(array.length); 148 for (Object v : array) { 149 v = v instanceof Calendar ? castToDate((Calendar) v) : null; 150 dates.add((Calendar) v); 151 } 152 return dates.toArray(); 153 } 154 } else if (value == null && type instanceof ListType && ((ListType) type).isArray()) { 155 // don't use null, as list-based matches don't use ternary logic 156 return new Object[0]; 157 } else { 158 return value; 159 } 160 } 161 162 protected Calendar castToDate(Calendar date) { 163 date.set(Calendar.HOUR_OF_DAY, 0); 164 date.set(Calendar.MINUTE, 0); 165 date.set(Calendar.SECOND, 0); 166 date.set(Calendar.MILLISECOND, 0); 167 return date; 168 } 169 170 @Override 171 public String toString() { 172 return "ValueInfo(" + canonRef + " " + steps + " = " + value + ")"; 173 } 174 } 175 176 /** 177 * Info about an iterator and how to compute it from a state. 178 * <p> 179 * The iterator iterates over a list of states or scalars and can be reset to a new list. 180 * <p> 181 * Also contains information about dependent values and iterators. 182 */ 183 protected static final class IterInfo implements Iterator<Object> { 184 185 /** 186 * Traversed steps to compute this iterator list from a state. Traversal steps can be: 187 * <ul> 188 * <li>String: a map key. 189 * <li>Integer: a list element. 190 * </ul> 191 */ 192 public final List<Serializable> steps; 193 194 public final List<ValueInfo> dependentValueInfos = new ArrayList<>(2); 195 196 public final List<IterInfo> dependentIterInfos = new ArrayList<>(2); 197 198 protected List<Object> list; 199 200 protected Iterator<Object> it; 201 202 public IterInfo(List<Serializable> steps) { 203 this.steps = steps; 204 } 205 206 public void setList(Object list) { 207 if (list == null) { 208 this.list = Collections.emptyList(); 209 } else if (list instanceof List) { 210 @SuppressWarnings("unchecked") 211 List<Object> stateList = (List<Object>) list; 212 this.list = stateList; 213 } else { 214 this.list = Arrays.asList((Object[]) list); 215 } 216 reset(); 217 } 218 219 public void reset() { 220 it = list.iterator(); 221 } 222 223 @Override 224 public boolean hasNext() { 225 return it.hasNext(); 226 } 227 228 @Override 229 public Object next() { 230 return it.next(); 231 } 232 233 @Override 234 public String toString() { 235 return "IterInfo(" + System.identityHashCode(this) + "," + steps + ")"; 236 } 237 } 238 239 protected static class DBSPathResolver implements PathResolver { 240 protected final DBSSession session; 241 242 public DBSPathResolver(DBSSession session) { 243 this.session = session; 244 } 245 246 @Override 247 public String getIdForPath(String path) { 248 return session.getDocumentIdByPath(path); 249 } 250 } 251 252 /** For each encountered reference in traversal order, the corresponding value info. */ 253 protected List<ValueInfo> referenceValueInfos; 254 255 /** Map of canonical reference to value info. */ 256 protected Map<String, ValueInfo> canonicalReferenceValueInfos; 257 258 /** Map of canonical reference prefix to iterator. */ 259 protected Map<String, IterInfo> canonicalPrefixIterInfos; 260 261 /** List of all iterators, in reversed order. */ 262 protected List<IterInfo> allIterInfos; 263 264 /** The toplevel iterators. */ 265 protected List<IterInfo> toplevelIterInfos; 266 267 /** The toplevel values, computed without wildcards. */ 268 protected List<ValueInfo> toplevelValueInfos; 269 270 // correlation to use for each uncorrelated wildcard (negative to avoid collisions with correlated ones) 271 protected int uncorrelatedCounter; 272 273 // did we find a wildcard in the SELECT projection or WHERE expression 274 protected boolean hasWildcard; 275 276 // which reference index is being visited, reset / updated during each pass 277 protected int refCount; 278 279 public DBSExpressionEvaluator(DBSSession session, SelectClause selectClause, Expression expression, 280 OrderByClause orderByClause, String[] principals, boolean fulltextSearchDisabled) { 281 super(new DBSPathResolver(session), principals, fulltextSearchDisabled); 282 this.selectClause = selectClause; 283 this.expression = expression; 284 this.orderByClause = orderByClause; 285 } 286 287 public SelectClause getSelectClause() { 288 return selectClause; 289 } 290 291 public Expression getExpression() { 292 return expression; 293 } 294 295 public OrderByClause getOrderByClause() { 296 return orderByClause; 297 } 298 299 protected List<String> getDocumentTypes() { 300 // TODO precompute in SchemaManager 301 if (documentTypes == null) { 302 documentTypes = new ArrayList<>(); 303 for (DocumentType docType : schemaManager.getDocumentTypes()) { 304 documentTypes.add(docType.getName()); 305 } 306 } 307 return documentTypes; 308 } 309 310 protected Set<String> getMixinDocumentTypes(String mixin) { 311 Set<String> types = schemaManager.getDocumentTypeNamesForFacet(mixin); 312 return types == null ? Collections.emptySet() : types; 313 } 314 315 protected boolean isNeverPerInstanceMixin(String mixin) { 316 return schemaManager.getNoPerDocumentQueryFacets().contains(mixin); 317 } 318 319 /** 320 * Initializes parsing datastructures. 321 */ 322 public void parse() { 323 schemaManager = Framework.getService(SchemaManager.class); 324 325 referenceValueInfos = new ArrayList<>(); 326 canonicalReferenceValueInfos = new HashMap<>(); 327 allIterInfos = new ArrayList<>(); 328 toplevelIterInfos = new ArrayList<>(); 329 toplevelValueInfos = new ArrayList<>(); 330 canonicalPrefixIterInfos = new HashMap<>(); 331 332 uncorrelatedCounter = -1; 333 hasWildcard = false; 334 335 // we do parsing using the ExpressionEvaluator to be sure that references 336 // are visited in the same order as when we'll do actual expression evaluation 337 parsing = true; 338 walkAll(); 339 parsing = false; 340 341 // we use all iterators in reversed ordered to increment them lexicographically from the end 342 Collections.reverse(allIterInfos); 343 } 344 345 /** 346 * Returns the projection matches for a given state. 347 */ 348 public List<Map<String, Serializable>> matches(State state) { 349 if (!checkSecurity(state)) { 350 return Collections.emptyList(); 351 } 352 this.state = state; // needed for mixin types evaluation 353 354 // initializes values and wildcards 355 initializeValuesAndIterators(state); 356 357 List<Map<String, Serializable>> matches = new ArrayList<>(); 358 for (;;) { 359 Map<String, Serializable> projection = walkAll(); 360 if (projection != null) { 361 matches.add(projection); 362 } 363 if (!hasWildcard) { 364 // all projections will be the same, get at most one 365 break; 366 } 367 boolean finished = incrementIterators(); 368 if (finished) { 369 break; 370 } 371 } 372 return matches; 373 } 374 375 protected boolean checkSecurity(State state) { 376 if (principals == null) { 377 return true; 378 } 379 String[] racl = (String[]) state.get(KEY_READ_ACL); 380 if (racl == null) { 381 log.error("NULL racl for " + state.get(KEY_ID)); 382 return false; 383 } 384 for (String user : racl) { 385 if (principals.contains(user)) { 386 return true; 387 } 388 } 389 return false; 390 } 391 392 /** 393 * Does one walk of the expression, using the wildcardIndexes currently defined. 394 */ 395 protected Map<String, Serializable> walkAll() { 396 refCount = 0; 397 Map<String, Serializable> projection = walkSelectClauseAndOrderBy(selectClause, orderByClause); 398 Object res = walkExpression(expression); 399 if (TRUE.equals(res)) { 400 // returns one match 401 return projection; 402 } else { 403 return null; 404 } 405 } 406 407 /** 408 * Walks the select clause and order by clause, and returns the projection. 409 */ 410 public Map<String, Serializable> walkSelectClauseAndOrderBy(SelectClause selectClause, 411 OrderByClause orderByClause) { 412 Map<String, Serializable> projection = new HashMap<>(); 413 boolean projectionOnFulltextScore = false; 414 boolean sortOnFulltextScore = false; 415 SelectList elements = selectClause.getSelectList(); 416 for (Operand op : elements.values()) { 417 if (op instanceof Reference) { 418 Reference ref = (Reference) op; 419 if (ref.name.equals(NXQL.ECM_FULLTEXT_SCORE)) { 420 projectionOnFulltextScore = true; 421 } 422 addProjection(ref, projection); 423 } 424 } 425 if (orderByClause != null) { 426 for (OrderByExpr obe : orderByClause.elements) { 427 Reference ref = obe.reference; 428 if (ref.name.equals(NXQL.ECM_FULLTEXT_SCORE)) { 429 sortOnFulltextScore = true; 430 } 431 addProjection(ref, projection); 432 } 433 } 434 if (projectionOnFulltextScore || sortOnFulltextScore) { 435 if (!parsing) { 436 if (!hasFulltext) { 437 throw new QueryParseException( 438 NXQL.ECM_FULLTEXT_SCORE + " cannot be used without " + NXQL.ECM_FULLTEXT); 439 } 440 projection.put(NXQL.ECM_FULLTEXT_SCORE, Double.valueOf(1)); 441 } 442 } 443 return projection; 444 } 445 446 protected void addProjection(Reference ref, Map<String, Serializable> projection) { 447 String name = ref.name; 448 if (name.equals(NXQL.ECM_PATH)) { 449 // ecm:path is special, computed and not stored in database 450 if (!parsing) { 451 // to compute PATH we need NAME, ID and PARENT_ID for all states 452 projection.put(NXQL.ECM_NAME, state.get(KEY_NAME)); 453 projection.put(NXQL.ECM_UUID, state.get(KEY_ID)); 454 projection.put(NXQL.ECM_PARENTID, state.get(KEY_PARENT_ID)); 455 } 456 return; 457 } 458 ValueInfo valueInfo = walkReferenceGetValueInfo(ref); 459 if (!parsing) { 460 projection.put(valueInfo.nxqlProp, (Serializable) valueInfo.value); 461 } 462 } 463 464 public boolean hasWildcardProjection() { 465 return selectClause.getSelectList().values().stream().anyMatch( 466 operand -> operand instanceof Reference && ((Reference) operand).name.contains("*")); 467 } 468 469 @Override 470 public Object walkReference(Reference ref) { 471 return walkReferenceGetValueInfo(ref).getValueForEvaluation(); 472 } 473 474 protected ValueInfo walkReferenceGetValueInfo(Reference ref) { 475 if (parsing) { 476 ValueInfo valueInfo = parseReference(ref); 477 referenceValueInfos.add(valueInfo); 478 return valueInfo; 479 } else { 480 return referenceValueInfos.get(refCount++); 481 } 482 } 483 484 /** 485 * Parses and computes value and iterator information for a reference. 486 */ 487 protected ValueInfo parseReference(Reference ref) { 488 ValueInfo parsed = parseReference(ref.name); 489 if (DATE_CAST.equals(ref.cast)) { 490 Type type = parsed.type; 491 if (!(type instanceof DateType 492 || (type instanceof ListType && ((ListType) type).getFieldType() instanceof DateType))) { 493 throw new QueryParseException("Cannot cast to " + ref.cast + ": " + ref.name); 494 } 495 parsed.isDateCast = true; 496 } 497 498 return canonicalReferenceValueInfos.computeIfAbsent(parsed.canonRef, k -> { 499 List<IterInfo> iterInfos = toplevelIterInfos; 500 List<ValueInfo> valueInfos = toplevelValueInfos; 501 List<String> prefix = new ArrayList<>(3); // canonical prefix 502 List<Serializable> steps = new ArrayList<>(1); 503 for (Serializable step : parsed.steps) { 504 if (step instanceof String) { 505 // complex sub-property 506 prefix.add((String) step); 507 steps.add(step); 508 continue; 509 } 510 if (step instanceof Integer) { 511 // explicit list index 512 prefix.add(step.toString()); 513 steps.add(step); 514 continue; 515 } 516 // wildcard 517 hasWildcard = true; 518 prefix.add("*" + step); 519 String canonPrefix = StringUtils.join(prefix, '/'); 520 IterInfo iter = canonicalPrefixIterInfos.get(canonPrefix); 521 if (iter == null) { 522 // first time we see this wildcard prefix, use a new iterator 523 iter = new IterInfo(steps); 524 canonicalPrefixIterInfos.put(canonPrefix, iter); 525 allIterInfos.add(iter); 526 iterInfos.add(iter); 527 } 528 iterInfos = iter.dependentIterInfos; 529 valueInfos = iter.dependentValueInfos; 530 // reset traversal for next cycle 531 steps = new ArrayList<>(); 532 } 533 // truncate traversal to steps since last wildcard, may be empty if referencing wildcard list directly 534 parsed.steps = steps; 535 valueInfos.add(parsed); 536 return parsed; 537 }); 538 } 539 540 /** 541 * Gets the canonical reference and parsed reference for this reference name. 542 * <p> 543 * The parsed reference is a list of components to traverse to get the value: 544 * <ul> 545 * <li>String = map key 546 * <li>Integer = list element 547 * <li>Long = wildcard correlation number (pos/neg) 548 * </ul> 549 * 550 * @return the canonical reference (with resolved uncorrelated wildcards) 551 */ 552 protected ValueInfo parseReference(String name) { 553 String[] parts = name.split("/"); 554 555 // convert first part to internal representation, and canonicalize prefixed schema 556 String prop = parts[0]; 557 Type type; 558 boolean isTrueOrNullBoolean; 559 if (prop.startsWith(NXQL.ECM_PREFIX)) { 560 prop = DBSSession.convToInternal(prop); 561 if (prop.equals(KEY_ACP)) { 562 return parseACP(parts, name); 563 } 564 type = DBSSession.getType(prop); 565 isTrueOrNullBoolean = true; 566 } else { 567 Field field = schemaManager.getField(prop); 568 if (field == null) { 569 if (prop.indexOf(':') > -1) { 570 throw new QueryParseException("No such property: " + name); 571 } 572 // check without prefix 573 // TODO precompute this in SchemaManagerImpl 574 for (Schema schema : schemaManager.getSchemas()) { 575 if (!StringUtils.isBlank(schema.getNamespace().prefix)) { 576 // schema with prefix, do not consider as candidate 577 continue; 578 } 579 if (schema != null) { 580 field = schema.getField(prop); 581 if (field != null) { 582 break; 583 } 584 } 585 } 586 if (field == null) { 587 throw new QueryParseException("No such property: " + name); 588 } 589 } 590 type = field.getType(); 591 isTrueOrNullBoolean = false; 592 prop = field.getName().getPrefixedName(); 593 } 594 parts[0] = prop; 595 596 // canonical prefix used to find shared values (foo/*1 referenced twice always uses the same value) 597 List<String> canonParts = new ArrayList<>(parts.length); 598 List<Serializable> steps = new ArrayList<>(parts.length); 599 boolean firstPart = true; 600 for (String part : parts) { 601 int c = part.indexOf('['); 602 if (c >= 0) { 603 // compat xpath foo[123] -> 123 604 part = part.substring(c + 1, part.length() - 1); 605 } 606 Serializable step; 607 if (NumberUtils.isDigits(part)) { 608 // explicit list index 609 step = Integer.valueOf(part); 610 type = ((ListType) type).getFieldType(); 611 } else if (!part.startsWith("*")) { 612 // complex sub-property 613 step = part; 614 if (!firstPart) { 615 // we already computed the type of the first part 616 Field field = ((ComplexType) type).getField(part); 617 if (field == null) { 618 throw new QueryParseException("No such property: " + name); 619 } 620 type = field.getType(); 621 } 622 } else { 623 // wildcard 624 int corr; 625 if (part.length() == 1) { 626 // uncorrelated wildcard 627 corr = uncorrelatedCounter--; // negative 628 part = "*" + corr; // unique correlation 629 } else { 630 // correlated wildcard, use correlation number 631 String digits = part.substring(1); 632 if (!NumberUtils.isDigits(digits)) { 633 throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); 634 } 635 corr = Integer.parseInt(digits); 636 if (corr < 0) { 637 throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); 638 } 639 } 640 step = Long.valueOf(corr); 641 type = ((ListType) type).getFieldType(); 642 } 643 canonParts.add(part); 644 steps.add(step); 645 firstPart = false; 646 } 647 String canonRef = StringUtils.join(canonParts, '/'); 648 ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); 649 valueInfo.type = type; 650 valueInfo.isTrueOrNullBoolean = isTrueOrNullBoolean; 651 return valueInfo; 652 } 653 654 protected ValueInfo parseACP(String[] parts, String name) { 655 if (parts.length != 3) { 656 throw new QueryParseException("No such property: " + name); 657 } 658 659 String wildcard = parts[1]; 660 if (NumberUtils.isDigits(wildcard)) { 661 throw new QueryParseException("Cannot use explicit index in ACLs: " + name); 662 } 663 int corr; 664 if (wildcard.length() == 1) { 665 // uncorrelated wildcard 666 corr = uncorrelatedCounter--; // negative 667 wildcard = "*" + corr; // unique correlation 668 } else { 669 // correlated wildcard, use correlation number 670 String digits = wildcard.substring(1); 671 if (!NumberUtils.isDigits(digits)) { 672 throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); 673 } 674 corr = Integer.parseInt(digits); 675 if (corr < 0) { 676 throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); 677 } 678 } 679 680 String subPart = DBSSession.convToInternalAce(parts[2]); 681 if (subPart == null) { 682 throw new QueryParseException("No such property: " + name); 683 } 684 List<Serializable> steps; 685 String canonRef; 686 if (subPart.equals(KEY_ACL_NAME)) { 687 steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL_NAME)); 688 canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL_NAME; 689 } else { 690 // for the second iterator we want a correlation number tied to the first one 691 int corr2 = corr * 1000000; 692 String wildcard2 = "*" + corr2; 693 steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL, Long.valueOf(corr2), subPart)); 694 canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL + '/' + wildcard2 + '/' + subPart; 695 } 696 ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); 697 valueInfo.type = DBSSession.getType(subPart); 698 valueInfo.isTrueOrNullBoolean = false; // TODO check ok 699 return valueInfo; 700 } 701 702 /** 703 * Initializes toplevel values and iterators for a given state. 704 */ 705 protected void initializeValuesAndIterators(State state) { 706 init(state, toplevelValueInfos, toplevelIterInfos); 707 } 708 709 /** 710 * Initializes values and iterators for a given state. 711 */ 712 protected void init(Object state, List<ValueInfo> valueInfos, List<IterInfo> iterInfos) { 713 for (ValueInfo valueInfo : valueInfos) { 714 valueInfo.value = traverse(state, valueInfo.steps); 715 } 716 for (IterInfo iterInfo : iterInfos) { 717 Object value = traverse(state, iterInfo.steps); 718 iterInfo.setList(value); 719 Object iterState = iterInfo.hasNext() ? iterInfo.next() : null; 720 init(iterState, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); 721 } 722 } 723 724 /** 725 * Traverses an object in a series of steps. 726 */ 727 protected Object traverse(Object value, List<Serializable> steps) { 728 for (Serializable step : steps) { 729 value = traverse(value, step); 730 } 731 return value; 732 } 733 734 /** 735 * Traverses a single step. 736 */ 737 protected Object traverse(Object value, Serializable step) { 738 if (step instanceof String) { 739 // complex sub-property 740 if (value != null && !(value instanceof State)) { 741 throw new QueryParseException("Invalid property " + step + " (no State but " + value.getClass() + ")"); 742 } 743 return value == null ? null : ((State) value).get(step); 744 } else if (step instanceof Integer) { 745 // explicit list index 746 int index = ((Integer) step).intValue(); 747 if (value == null) { 748 return null; 749 } else if (value instanceof List) { 750 @SuppressWarnings("unchecked") 751 List<Serializable> list = (List<Serializable>) value; 752 if (index >= list.size()) { 753 return null; 754 } else { 755 return list.get(index); 756 } 757 } else if (value instanceof Object[]) { 758 Object[] array = (Object[]) value; 759 if (index >= array.length) { 760 return null; 761 } else { 762 return array[index]; 763 } 764 } else { 765 throw new QueryParseException( 766 "Invalid property " + step + " (no List/array but " + value.getClass() + ")"); 767 } 768 } else { 769 throw new QueryParseException("Invalid step " + step + " (unknown class " + step.getClass() + ")"); 770 } 771 } 772 773 /** 774 * Increments iterators lexicographically. 775 * <p> 776 * Returns {@code true} when all iterations are finished. 777 */ 778 protected boolean incrementIterators() { 779 // we iterate on a pre-reversed allIterInfos list as this ensure that 780 // dependent iterators are incremented before those that control them 781 boolean more = false; 782 for (IterInfo iterInfo : allIterInfos) { 783 more = iterInfo.hasNext(); 784 if (!more) { 785 // end of this iterator, reset and !more will carry to next one 786 iterInfo.reset(); 787 } 788 // get the current value, if any 789 Object state = iterInfo.hasNext() ? iterInfo.next() : null; 790 // recompute dependent stuff 791 init(state, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); 792 if (more) { 793 break; 794 } 795 } 796 return !more; 797 } 798 799 /** 800 * {@inheritDoc} 801 * <p> 802 * ecm:mixinTypes IN ('Foo', 'Bar') 803 * <p> 804 * primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%' 805 * <p> 806 * ecm:mixinTypes NOT IN ('Foo', 'Bar') 807 * <p> 808 * primarytype IN (... types without Foo nor Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes NOT LIKE 809 * '%Bar%' OR mixintypes IS NULL) 810 */ 811 @Override 812 public Boolean walkMixinTypes(List<String> mixins, boolean include) { 813 if (parsing) { 814 return null; 815 } 816 /* 817 * Primary types that match. 818 */ 819 Set<String> matchPrimaryTypes; 820 if (include) { 821 matchPrimaryTypes = new HashSet<>(); 822 for (String mixin : mixins) { 823 matchPrimaryTypes.addAll(getMixinDocumentTypes(mixin)); 824 } 825 } else { 826 matchPrimaryTypes = new HashSet<>(getDocumentTypes()); 827 for (String mixin : mixins) { 828 matchPrimaryTypes.removeAll(getMixinDocumentTypes(mixin)); 829 } 830 } 831 /* 832 * Instance mixins that match. 833 */ 834 Set<String> matchMixinTypes = new HashSet<>(); 835 for (String mixin : mixins) { 836 if (!isNeverPerInstanceMixin(mixin)) { 837 matchMixinTypes.add(mixin); 838 } 839 } 840 /* 841 * Evaluation. 842 */ 843 String primaryType = (String) state.get(KEY_PRIMARY_TYPE); 844 Object[] mixinTypesArray = (Object[]) state.get(KEY_MIXIN_TYPES); 845 List<Object> mixinTypes = mixinTypesArray == null ? Collections.emptyList() : Arrays.asList(mixinTypesArray); 846 if (include) { 847 // primary types 848 if (matchPrimaryTypes.contains(primaryType)) { 849 return TRUE; 850 } 851 // mixin types 852 matchMixinTypes.retainAll(mixinTypes); // intersection 853 return Boolean.valueOf(!matchMixinTypes.isEmpty()); 854 } else { 855 // primary types 856 if (!matchPrimaryTypes.contains(primaryType)) { 857 return FALSE; 858 } 859 // mixin types 860 matchMixinTypes.retainAll(mixinTypes); // intersection 861 return Boolean.valueOf(matchMixinTypes.isEmpty()); 862 } 863 } 864 865 @Override 866 public String toString() { 867 StringBuilder sb = new StringBuilder("SELECT "); 868 sb.append(selectClause); 869 sb.append(" WHERE "); 870 if (expression instanceof MultiExpression) { 871 for (Iterator<Operand> it = ((MultiExpression) expression).values.iterator(); it.hasNext();) { 872 Operand operand = it.next(); 873 sb.append(operand.toString()); 874 if (it.hasNext()) { 875 sb.append(" AND "); 876 } 877 } 878 } else { 879 sb.append(expression); 880 } 881 if (orderByClause != null) { 882 sb.append(" ORDER BY "); 883 sb.append(orderByClause); 884 } 885 return sb.toString(); 886 } 887 888}