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