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