001/* 002 * (C) Copyright 2014 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 fulltextDisabled) { 281 super(new DBSPathResolver(session), principals, fulltextDisabled); 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 SelectList elements = selectClause.getSelectList(); 414 for (int i = 0; i < elements.size(); i++) { 415 Operand op = elements.get(i); 416 if (op instanceof Reference) { 417 addProjection((Reference) op, projection); 418 } 419 } 420 if (orderByClause != null) { 421 for (OrderByExpr obe : orderByClause.elements) { 422 addProjection(obe.reference, projection); 423 } 424 } 425 return projection; 426 } 427 428 protected void addProjection(Reference ref, Map<String, Serializable> projection) { 429 String name = ref.name; 430 if (name.equals(NXQL.ECM_PATH)) { 431 // ecm:path is special, computed and not stored in database 432 if (!parsing) { 433 // to compute PATH we need NAME, ID and PARENT_ID for all states 434 projection.put(NXQL.ECM_NAME, state.get(KEY_NAME)); 435 projection.put(NXQL.ECM_UUID, state.get(KEY_ID)); 436 projection.put(NXQL.ECM_PARENTID, state.get(KEY_PARENT_ID)); 437 } 438 return; 439 } 440 ValueInfo valueInfo = walkReferenceGetValueInfo(ref); 441 if (!parsing) { 442 projection.put(valueInfo.nxqlProp, (Serializable) valueInfo.value); 443 } 444 } 445 446 public boolean hasWildcardProjection() { 447 SelectList elements = selectClause.getSelectList(); 448 for (int i = 0; i < elements.size(); i++) { 449 Operand op = elements.get(i); 450 if (op instanceof Reference) { 451 if (((Reference) op).name.contains("*")) { 452 return true; 453 } 454 } 455 } 456 return false; 457 } 458 459 @Override 460 public Object walkReference(Reference ref) { 461 return walkReferenceGetValueInfo(ref).getValueForEvaluation(); 462 } 463 464 protected ValueInfo walkReferenceGetValueInfo(Reference ref) { 465 if (parsing) { 466 ValueInfo valueInfo = parseReference(ref); 467 referenceValueInfos.add(valueInfo); 468 return valueInfo; 469 } else { 470 return referenceValueInfos.get(refCount++); 471 } 472 } 473 474 /** 475 * Parses and computes value and iterator information for a reference. 476 */ 477 protected ValueInfo parseReference(Reference ref) { 478 ValueInfo parsed = parseReference(ref.name); 479 if (DATE_CAST.equals(ref.cast)) { 480 Type type = parsed.type; 481 if (!(type instanceof DateType 482 || (type instanceof ListType && ((ListType) type).getFieldType() instanceof DateType))) { 483 throw new QueryParseException("Cannot cast to " + ref.cast + ": " + ref.name); 484 } 485 parsed.isDateCast = true; 486 } 487 488 ValueInfo valueInfo = canonicalReferenceValueInfos.computeIfAbsent(parsed.canonRef, k -> { 489 List<IterInfo> iterInfos = toplevelIterInfos; 490 List<ValueInfo> valueInfos = toplevelValueInfos; 491 List<String> prefix = new ArrayList<>(3); // canonical prefix 492 List<Serializable> steps = new ArrayList<>(1); 493 for (Serializable step : parsed.steps) { 494 if (step instanceof String) { 495 // complex sub-property 496 prefix.add((String) step); 497 steps.add(step); 498 continue; 499 } 500 if (step instanceof Integer) { 501 // explicit list index 502 prefix.add(step.toString()); 503 steps.add(step); 504 continue; 505 } 506 // wildcard 507 hasWildcard = true; 508 prefix.add("*" + step); 509 String canonPrefix = StringUtils.join(prefix, '/'); 510 IterInfo iter = canonicalPrefixIterInfos.get(canonPrefix); 511 if (iter == null) { 512 // first time we see this wildcard prefix, use a new iterator 513 iter = new IterInfo(steps); 514 canonicalPrefixIterInfos.put(canonPrefix, iter); 515 allIterInfos.add(iter); 516 iterInfos.add(iter); 517 } 518 iterInfos = iter.dependentIterInfos; 519 valueInfos = iter.dependentValueInfos; 520 // reset traversal for next cycle 521 steps = new ArrayList<>(); 522 } 523 // truncate traversal to steps since last wildcard, may be empty if referencing wildcard list directly 524 parsed.steps = steps; 525 valueInfos.add(parsed); 526 return parsed; 527 }); 528 return valueInfo; 529 } 530 531 /** 532 * Gets the canonical reference and parsed reference for this reference name. 533 * <p> 534 * The parsed reference is a list of components to traverse to get the value: 535 * <ul> 536 * <li>String = map key 537 * <li>Integer = list element 538 * <li>Long = wildcard correlation number (pos/neg) 539 * </ul> 540 * 541 * @return the canonical reference (with resolved uncorrelated wildcards) 542 */ 543 protected ValueInfo parseReference(String name) { 544 String[] parts = name.split("/"); 545 546 // convert first part to internal representation, and canonicalize prefixed schema 547 String prop = parts[0]; 548 Type type; 549 boolean isTrueOrNullBoolean; 550 if (prop.startsWith(NXQL.ECM_PREFIX)) { 551 prop = DBSSession.convToInternal(prop); 552 if (prop.equals(KEY_ACP)) { 553 return parseACP(parts, name); 554 } 555 type = DBSSession.getType(prop); 556 isTrueOrNullBoolean = true; 557 } else { 558 Field field = schemaManager.getField(prop); 559 if (field == null) { 560 if (prop.indexOf(':') > -1) { 561 throw new QueryParseException("No such property: " + name); 562 } 563 // check without prefix 564 // TODO precompute this in SchemaManagerImpl 565 for (Schema schema : schemaManager.getSchemas()) { 566 if (!StringUtils.isBlank(schema.getNamespace().prefix)) { 567 // schema with prefix, do not consider as candidate 568 continue; 569 } 570 if (schema != null) { 571 field = schema.getField(prop); 572 if (field != null) { 573 break; 574 } 575 } 576 } 577 if (field == null) { 578 throw new QueryParseException("No such property: " + name); 579 } 580 } 581 type = field.getType(); 582 isTrueOrNullBoolean = false; 583 prop = field.getName().getPrefixedName(); 584 } 585 parts[0] = prop; 586 587 // canonical prefix used to find shared values (foo/*1 referenced twice always uses the same value) 588 List<String> canonParts = new ArrayList<>(parts.length); 589 List<Serializable> steps = new ArrayList<>(parts.length); 590 boolean firstPart = true; 591 for (String part : parts) { 592 int c = part.indexOf('['); 593 if (c >= 0) { 594 // compat xpath foo[123] -> 123 595 part = part.substring(c + 1, part.length() - 1); 596 } 597 Serializable step; 598 if (NumberUtils.isDigits(part)) { 599 // explicit list index 600 step = Integer.valueOf(part); 601 type = ((ListType) type).getFieldType(); 602 } else if (!part.startsWith("*")) { 603 // complex sub-property 604 step = part; 605 if (!firstPart) { 606 // we already computed the type of the first part 607 Field field = ((ComplexType) type).getField(part); 608 if (field == null) { 609 throw new QueryParseException("No such property: " + name); 610 } 611 type = field.getType(); 612 } 613 } else { 614 // wildcard 615 int corr; 616 if (part.length() == 1) { 617 // uncorrelated wildcard 618 corr = uncorrelatedCounter--; // negative 619 part = "*" + corr; // unique correlation 620 } else { 621 // correlated wildcard, use correlation number 622 String digits = part.substring(1); 623 if (!NumberUtils.isDigits(digits)) { 624 throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); 625 } 626 corr = Integer.parseInt(digits); 627 if (corr < 0) { 628 throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); 629 } 630 } 631 step = Long.valueOf(corr); 632 type = ((ListType) type).getFieldType(); 633 } 634 canonParts.add(part); 635 steps.add(step); 636 firstPart = false; 637 } 638 String canonRef = StringUtils.join(canonParts, '/'); 639 ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); 640 valueInfo.type = type; 641 valueInfo.isTrueOrNullBoolean = isTrueOrNullBoolean; 642 return valueInfo; 643 } 644 645 protected ValueInfo parseACP(String[] parts, String name) { 646 if (parts.length != 3) { 647 throw new QueryParseException("No such property: " + name); 648 } 649 650 String wildcard = parts[1]; 651 if (NumberUtils.isDigits(wildcard)) { 652 throw new QueryParseException("Cannot use explicit index in ACLs: " + name); 653 } 654 int corr; 655 if (wildcard.length() == 1) { 656 // uncorrelated wildcard 657 corr = uncorrelatedCounter--; // negative 658 wildcard = "*" + corr; // unique correlation 659 } else { 660 // correlated wildcard, use correlation number 661 String digits = wildcard.substring(1); 662 if (!NumberUtils.isDigits(digits)) { 663 throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); 664 } 665 corr = Integer.parseInt(digits); 666 if (corr < 0) { 667 throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); 668 } 669 } 670 671 String subPart = DBSSession.convToInternalAce(parts[2]); 672 if (subPart == null) { 673 throw new QueryParseException("No such property: " + name); 674 } 675 List<Serializable> steps; 676 String canonRef; 677 if (subPart.equals(KEY_ACL_NAME)) { 678 steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL_NAME)); 679 canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL_NAME; 680 } else { 681 // for the second iterator we want a correlation number tied to the first one 682 int corr2 = corr * 1000000; 683 String wildcard2 = "*" + corr2; 684 steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL, Long.valueOf(corr2), subPart)); 685 canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL + '/' + wildcard2 + '/' + subPart; 686 } 687 ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); 688 valueInfo.type = DBSSession.getType(subPart); 689 valueInfo.isTrueOrNullBoolean = false; // TODO check ok 690 return valueInfo; 691 } 692 693 /** 694 * Initializes toplevel values and iterators for a given state. 695 */ 696 protected void initializeValuesAndIterators(State state) { 697 init(state, toplevelValueInfos, toplevelIterInfos); 698 } 699 700 /** 701 * Initializes values and iterators for a given state. 702 */ 703 protected void init(Object state, List<ValueInfo> valueInfos, List<IterInfo> iterInfos) { 704 for (ValueInfo valueInfo : valueInfos) { 705 valueInfo.value = traverse(state, valueInfo.steps); 706 } 707 for (IterInfo iterInfo : iterInfos) { 708 Object value = traverse(state, iterInfo.steps); 709 iterInfo.setList(value); 710 Object iterState = iterInfo.hasNext() ? iterInfo.next() : null; 711 init(iterState, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); 712 } 713 } 714 715 /** 716 * Traverses an object in a series of steps. 717 */ 718 protected Object traverse(Object value, List<Serializable> steps) { 719 for (Serializable step : steps) { 720 value = traverse(value, step); 721 } 722 return value; 723 } 724 725 /** 726 * Traverses a single step. 727 */ 728 protected Object traverse(Object value, Serializable step) { 729 if (step instanceof String) { 730 // complex sub-property 731 if (value != null && !(value instanceof State)) { 732 throw new QueryParseException("Invalid property " + step + " (no State but " + value.getClass() + ")"); 733 } 734 return value == null ? null : ((State) value).get((String) step); 735 } else if (step instanceof Integer) { 736 // explicit list index 737 int index = ((Integer) step).intValue(); 738 if (value == null) { 739 return null; 740 } else if (value instanceof List) { 741 @SuppressWarnings("unchecked") 742 List<Serializable> list = (List<Serializable>) value; 743 if (index >= list.size()) { 744 return null; 745 } else { 746 return list.get(index); 747 } 748 } else if (value instanceof Object[]) { 749 Object[] array = (Object[]) value; 750 if (index >= array.length) { 751 return null; 752 } else { 753 return array[index]; 754 } 755 } else { 756 throw new QueryParseException( 757 "Invalid property " + step + " (no List/array but " + value.getClass() + ")"); 758 } 759 } else { 760 throw new QueryParseException("Invalid step " + step + " (unknown class " + step.getClass() + ")"); 761 } 762 } 763 764 /** 765 * Increments iterators lexicographically. 766 * <p> 767 * Returns {@code true} when all iterations are finished. 768 */ 769 protected boolean incrementIterators() { 770 // we iterate on a pre-reversed allIterInfos list as this ensure that 771 // dependent iterators are incremented before those that control them 772 boolean more = false; 773 for (IterInfo iterInfo : allIterInfos) { 774 more = iterInfo.hasNext(); 775 if (!more) { 776 // end of this iterator, reset and !more will carry to next one 777 iterInfo.reset(); 778 } 779 // get the current value, if any 780 Object state = iterInfo.hasNext() ? iterInfo.next() : null; 781 // recompute dependent stuff 782 init(state, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); 783 if (more) { 784 break; 785 } 786 } 787 return !more; 788 } 789 790 /** 791 * {@inheritDoc} 792 * <p> 793 * ecm:mixinTypes IN ('Foo', 'Bar') 794 * <p> 795 * primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%' 796 * <p> 797 * ecm:mixinTypes NOT IN ('Foo', 'Bar') 798 * <p> 799 * primarytype IN (... types without Foo nor Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes NOT LIKE 800 * '%Bar%' OR mixintypes IS NULL) 801 */ 802 @Override 803 public Boolean walkMixinTypes(List<String> mixins, boolean include) { 804 if (parsing) { 805 return null; 806 } 807 /* 808 * Primary types that match. 809 */ 810 Set<String> matchPrimaryTypes; 811 if (include) { 812 matchPrimaryTypes = new HashSet<String>(); 813 for (String mixin : mixins) { 814 matchPrimaryTypes.addAll(getMixinDocumentTypes(mixin)); 815 } 816 } else { 817 matchPrimaryTypes = new HashSet<String>(getDocumentTypes()); 818 for (String mixin : mixins) { 819 matchPrimaryTypes.removeAll(getMixinDocumentTypes(mixin)); 820 } 821 } 822 /* 823 * Instance mixins that match. 824 */ 825 Set<String> matchMixinTypes = new HashSet<String>(); 826 for (String mixin : mixins) { 827 if (!isNeverPerInstanceMixin(mixin)) { 828 matchMixinTypes.add(mixin); 829 } 830 } 831 /* 832 * Evaluation. 833 */ 834 String primaryType = (String) state.get(KEY_PRIMARY_TYPE); 835 Object[] mixinTypesArray = (Object[]) state.get(KEY_MIXIN_TYPES); 836 List<Object> mixinTypes = mixinTypesArray == null ? Collections.emptyList() : Arrays.asList(mixinTypesArray); 837 if (include) { 838 // primary types 839 if (matchPrimaryTypes.contains(primaryType)) { 840 return TRUE; 841 } 842 // mixin types 843 matchMixinTypes.retainAll(mixinTypes); // intersection 844 return Boolean.valueOf(!matchMixinTypes.isEmpty()); 845 } else { 846 // primary types 847 if (!matchPrimaryTypes.contains(primaryType)) { 848 return FALSE; 849 } 850 // mixin types 851 matchMixinTypes.retainAll(mixinTypes); // intersection 852 return Boolean.valueOf(matchMixinTypes.isEmpty()); 853 } 854 } 855 856 @Override 857 public String toString() { 858 StringBuilder sb = new StringBuilder("SELECT "); 859 sb.append(selectClause); 860 sb.append(" WHERE "); 861 if (expression instanceof MultiExpression) { 862 for (Iterator<Operand> it = ((MultiExpression) expression).values.iterator(); it.hasNext();) { 863 Operand operand = it.next(); 864 sb.append(operand.toString()); 865 if (it.hasNext()) { 866 sb.append(" AND "); 867 } 868 } 869 } else { 870 sb.append(expression); 871 } 872 if (orderByClause != null) { 873 sb.append(" ORDER BY "); 874 sb.append(orderByClause); 875 } 876 return sb.toString(); 877 } 878 879}