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.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.lang.StringUtils; 050import org.apache.commons.lang.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 if (schema != null) { 594 field = schema.getField(prop); 595 if (field != null) { 596 break; 597 } 598 } 599 } 600 if (field == null) { 601 throw new QueryParseException("No such property: " + name); 602 } 603 } 604 type = field.getType(); 605 isTrueOrNullBoolean = false; 606 prop = field.getName().getPrefixedName(); 607 } 608 parts[0] = prop; 609 610 // canonical prefix used to find shared values (foo/*1 referenced twice always uses the same value) 611 List<String> canonParts = new ArrayList<>(parts.length); 612 List<Serializable> steps = new ArrayList<>(parts.length); 613 boolean firstPart = true; 614 for (String part : parts) { 615 int c = part.indexOf('['); 616 if (c >= 0) { 617 // compat xpath foo[123] -> 123 618 part = part.substring(c + 1, part.length() - 1); 619 } 620 Serializable step; 621 if (NumberUtils.isDigits(part)) { 622 // explicit list index 623 step = Integer.valueOf(part); 624 type = ((ListType) type).getFieldType(); 625 } else if (!part.startsWith("*")) { 626 // complex sub-property 627 step = part; 628 if (firstPart) { 629 if (PROP_MAJOR_VERSION.equals(part) || PROP_MINOR_VERSION.equals(part)) { 630 step = DBSSession.convToInternal(part); 631 } 632 // we already computed the type of the first part 633 } else { 634 Field field = ((ComplexType) type).getField(part); 635 if (field == null) { 636 throw new QueryParseException("No such property: " + name); 637 } 638 type = field.getType(); 639 } 640 } else { 641 // wildcard 642 int corr; 643 if (part.length() == 1) { 644 // uncorrelated wildcard 645 corr = uncorrelatedCounter--; // negative 646 part = "*" + corr; // unique correlation 647 } else { 648 // correlated wildcard, use correlation number 649 String digits = part.substring(1); 650 if (!NumberUtils.isDigits(digits)) { 651 throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); 652 } 653 corr = Integer.parseInt(digits); 654 if (corr < 0) { 655 throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); 656 } 657 } 658 step = Long.valueOf(corr); 659 type = ((ListType) type).getFieldType(); 660 } 661 canonParts.add(part); 662 steps.add(step); 663 firstPart = false; 664 } 665 String canonRef = StringUtils.join(canonParts, '/'); 666 String nxqlProp = originalName == null ? name : originalName; 667 ValueInfo valueInfo = new ValueInfo(steps, nxqlProp, canonRef); 668 valueInfo.type = type; 669 valueInfo.isTrueOrNullBoolean = isTrueOrNullBoolean; 670 return valueInfo; 671 } 672 673 protected ValueInfo parseACP(String[] parts, String name) { 674 if (parts.length != 3) { 675 throw new QueryParseException("No such property: " + name); 676 } 677 678 String wildcard = parts[1]; 679 if (NumberUtils.isDigits(wildcard)) { 680 throw new QueryParseException("Cannot use explicit index in ACLs: " + name); 681 } 682 int corr; 683 if (wildcard.length() == 1) { 684 // uncorrelated wildcard 685 corr = uncorrelatedCounter--; // negative 686 wildcard = "*" + corr; // unique correlation 687 } else { 688 // correlated wildcard, use correlation number 689 String digits = wildcard.substring(1); 690 if (!NumberUtils.isDigits(digits)) { 691 throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); 692 } 693 corr = Integer.parseInt(digits); 694 if (corr < 0) { 695 throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); 696 } 697 } 698 699 String subPart = DBSSession.convToInternalAce(parts[2]); 700 if (subPart == null) { 701 throw new QueryParseException("No such property: " + name); 702 } 703 List<Serializable> steps; 704 String canonRef; 705 if (subPart.equals(KEY_ACL_NAME)) { 706 steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL_NAME)); 707 canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL_NAME; 708 } else { 709 // for the second iterator we want a correlation number tied to the first one 710 int corr2 = corr * 1000000; 711 String wildcard2 = "*" + corr2; 712 steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL, Long.valueOf(corr2), subPart)); 713 canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL + '/' + wildcard2 + '/' + subPart; 714 } 715 ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); 716 valueInfo.type = DBSSession.getType(subPart); 717 valueInfo.isTrueOrNullBoolean = false; // TODO check ok 718 return valueInfo; 719 } 720 721 /** 722 * Initializes toplevel values and iterators for a given state. 723 */ 724 protected void initializeValuesAndIterators(State state) { 725 init(state, toplevelValueInfos, toplevelIterInfos); 726 } 727 728 /** 729 * Initializes values and iterators for a given state. 730 */ 731 protected void init(Object state, List<ValueInfo> valueInfos, List<IterInfo> iterInfos) { 732 for (ValueInfo valueInfo : valueInfos) { 733 valueInfo.value = traverse(state, valueInfo.steps); 734 } 735 for (IterInfo iterInfo : iterInfos) { 736 Object value = traverse(state, iterInfo.steps); 737 iterInfo.setList(value); 738 Object iterState = iterInfo.hasNext() ? iterInfo.next() : null; 739 init(iterState, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); 740 } 741 } 742 743 /** 744 * Traverses an object in a series of steps. 745 */ 746 protected Object traverse(Object value, List<Serializable> steps) { 747 for (Serializable step : steps) { 748 value = traverse(value, step); 749 } 750 return value; 751 } 752 753 /** 754 * Traverses a single step. 755 */ 756 protected Object traverse(Object value, Serializable step) { 757 if (step instanceof String) { 758 // complex sub-property 759 if (value != null && !(value instanceof State)) { 760 throw new QueryParseException("Invalid property " + step + " (no State but " + value.getClass() + ")"); 761 } 762 return value == null ? null : ((State) value).get(step); 763 } else if (step instanceof Integer) { 764 // explicit list index 765 int index = ((Integer) step).intValue(); 766 if (value == null) { 767 return null; 768 } else if (value instanceof List) { 769 @SuppressWarnings("unchecked") 770 List<Serializable> list = (List<Serializable>) value; 771 if (index >= list.size()) { 772 return null; 773 } else { 774 return list.get(index); 775 } 776 } else if (value instanceof Object[]) { 777 Object[] array = (Object[]) value; 778 if (index >= array.length) { 779 return null; 780 } else { 781 return array[index]; 782 } 783 } else { 784 throw new QueryParseException( 785 "Invalid property " + step + " (no List/array but " + value.getClass() + ")"); 786 } 787 } else { 788 throw new QueryParseException("Invalid step " + step + " (unknown class " + step.getClass() + ")"); 789 } 790 } 791 792 /** 793 * Increments iterators lexicographically. 794 * <p> 795 * Returns {@code true} when all iterations are finished. 796 */ 797 protected boolean incrementIterators() { 798 // we iterate on a pre-reversed allIterInfos list as this ensure that 799 // dependent iterators are incremented before those that control them 800 boolean more = false; 801 for (IterInfo iterInfo : allIterInfos) { 802 more = iterInfo.hasNext(); 803 if (!more) { 804 // end of this iterator, reset and !more will carry to next one 805 iterInfo.reset(); 806 } 807 // get the current value, if any 808 Object state = iterInfo.hasNext() ? iterInfo.next() : null; 809 // recompute dependent stuff 810 init(state, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); 811 if (more) { 812 break; 813 } 814 } 815 return !more; 816 } 817 818 /** 819 * {@inheritDoc} 820 * <p> 821 * ecm:mixinTypes IN ('Foo', 'Bar') 822 * <p> 823 * primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%' 824 * <p> 825 * ecm:mixinTypes NOT IN ('Foo', 'Bar') 826 * <p> 827 * primarytype IN (... types without Foo nor Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes NOT LIKE 828 * '%Bar%' OR mixintypes IS NULL) 829 */ 830 @Override 831 public Boolean walkMixinTypes(List<String> mixins, boolean include) { 832 if (parsing) { 833 return null; 834 } 835 /* 836 * Primary types that match. 837 */ 838 Set<String> matchPrimaryTypes; 839 if (include) { 840 matchPrimaryTypes = new HashSet<>(); 841 for (String mixin : mixins) { 842 matchPrimaryTypes.addAll(getMixinDocumentTypes(mixin)); 843 } 844 } else { 845 matchPrimaryTypes = new HashSet<>(getDocumentTypes()); 846 for (String mixin : mixins) { 847 matchPrimaryTypes.removeAll(getMixinDocumentTypes(mixin)); 848 } 849 } 850 /* 851 * Instance mixins that match. 852 */ 853 Set<String> matchMixinTypes = new HashSet<>(); 854 for (String mixin : mixins) { 855 if (!isNeverPerInstanceMixin(mixin)) { 856 matchMixinTypes.add(mixin); 857 } 858 } 859 /* 860 * Evaluation. 861 */ 862 String primaryType = (String) state.get(KEY_PRIMARY_TYPE); 863 Object[] mixinTypesArray = (Object[]) state.get(KEY_MIXIN_TYPES); 864 List<Object> mixinTypes = mixinTypesArray == null ? Collections.emptyList() : Arrays.asList(mixinTypesArray); 865 if (include) { 866 // primary types 867 if (matchPrimaryTypes.contains(primaryType)) { 868 return TRUE; 869 } 870 // mixin types 871 matchMixinTypes.retainAll(mixinTypes); // intersection 872 return Boolean.valueOf(!matchMixinTypes.isEmpty()); 873 } else { 874 // primary types 875 if (!matchPrimaryTypes.contains(primaryType)) { 876 return FALSE; 877 } 878 // mixin types 879 matchMixinTypes.retainAll(mixinTypes); // intersection 880 return Boolean.valueOf(matchMixinTypes.isEmpty()); 881 } 882 } 883 884 @Override 885 public String toString() { 886 StringBuilder sb = new StringBuilder("SELECT "); 887 sb.append(selectClause); 888 sb.append(" WHERE "); 889 if (expression instanceof MultiExpression) { 890 for (Iterator<Operand> it = ((MultiExpression) expression).values.iterator(); it.hasNext();) { 891 Operand operand = it.next(); 892 sb.append(operand.toString()); 893 if (it.hasNext()) { 894 sb.append(" AND "); 895 } 896 } 897 } else { 898 sb.append(expression); 899 } 900 if (orderByClause != null) { 901 sb.append(" ORDER BY "); 902 sb.append(orderByClause); 903 } 904 return sb.toString(); 905 } 906 907}