001/* 002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012package org.nuxeo.ecm.core.opencmis.impl.server; 013 014import static org.apache.chemistry.opencmis.commons.enums.BaseTypeId.CMIS_DOCUMENT; 015import static org.apache.chemistry.opencmis.commons.enums.BaseTypeId.CMIS_RELATIONSHIP; 016 017import java.io.Serializable; 018import java.math.BigDecimal; 019import java.math.BigInteger; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Calendar; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.LinkedHashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.Set; 033 034import org.antlr.runtime.RecognitionException; 035import org.antlr.runtime.tree.Tree; 036import org.apache.chemistry.opencmis.commons.PropertyIds; 037import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition; 038import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition; 039import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer; 040import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; 041import org.apache.chemistry.opencmis.commons.enums.Cardinality; 042import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException; 043import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; 044import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalDefinitionImpl; 045import org.apache.chemistry.opencmis.server.support.query.AbstractPredicateWalker; 046import org.apache.chemistry.opencmis.server.support.query.CmisQueryWalker; 047import org.apache.chemistry.opencmis.server.support.query.CmisSelector; 048import org.apache.chemistry.opencmis.server.support.query.ColumnReference; 049import org.apache.chemistry.opencmis.server.support.query.FunctionReference; 050import org.apache.chemistry.opencmis.server.support.query.FunctionReference.CmisQlFunction; 051import org.apache.chemistry.opencmis.server.support.query.QueryObject; 052import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec; 053import org.apache.chemistry.opencmis.server.support.query.QueryUtil; 054import org.apache.commons.lang.StringUtils; 055import org.apache.commons.logging.Log; 056import org.apache.commons.logging.LogFactory; 057import org.joda.time.LocalDateTime; 058import org.joda.time.format.DateTimeFormatter; 059import org.joda.time.format.ISODateTimeFormat; 060import org.nuxeo.ecm.core.api.CoreSession; 061import org.nuxeo.ecm.core.api.DocumentRef; 062import org.nuxeo.ecm.core.api.IdRef; 063import org.nuxeo.ecm.core.api.IterableQueryResult; 064import org.nuxeo.ecm.core.api.LifeCycleConstants; 065import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl; 066import org.nuxeo.ecm.core.query.QueryParseException; 067import org.nuxeo.ecm.core.query.sql.NXQL; 068 069/** 070 * Transformer of CMISQL queries into NXQL queries. 071 */ 072public class CMISQLtoNXQL { 073 074 private static final Log log = LogFactory.getLog(CMISQLtoNXQL.class); 075 076 protected static final String CMIS_PREFIX = "cmis:"; 077 078 protected static final String NX_PREFIX = "nuxeo:"; 079 080 protected static final String NXQL_DOCUMENT = "Document"; 081 082 protected static final String NXQL_RELATION = "Relation"; 083 084 protected static final String NXQL_DC_TITLE = "dc:title"; 085 086 protected static final String NXQL_DC_DESCRIPTION = "dc:description"; 087 088 protected static final String NXQL_DC_CREATOR = "dc:creator"; 089 090 protected static final String NXQL_DC_CREATED = "dc:created"; 091 092 protected static final String NXQL_DC_MODIFIED = "dc:modified"; 093 094 protected static final String NXQL_DC_LAST_CONTRIBUTOR = "dc:lastContributor"; 095 096 protected static final String NXQL_REL_SOURCE = "relation:source"; 097 098 protected static final String NXQL_REL_TARGET = "relation:target"; 099 100 protected static final DateTimeFormatter ISO_DATE_TIME_FORMAT = ISODateTimeFormat.dateTime(); 101 102 private static final char QUOTE = '\''; 103 104 private static final String SPACE_ASC = " asc"; 105 106 private static final String SPACE_DESC = " desc"; 107 108 // list of SQL column where NULL (missing value) should be treated as 109 // Boolean.FALSE in a query result 110 protected static final Set<String> NULL_IS_FALSE_COLUMNS = new HashSet<String>(Arrays.asList(NXQL.ECM_ISVERSION, 111 NXQL.ECM_ISLATESTVERSION, NXQL.ECM_ISLATESTMAJORVERSION, NXQL.ECM_ISCHECKEDIN)); 112 113 protected Map<String, PropertyDefinition<?>> typeInfo; 114 115 protected CoreSession coreSession; 116 117 // ----- filled during walks of the clauses ----- 118 119 protected QueryObject query; 120 121 protected TypeDefinition fromType; 122 123 protected boolean skipDeleted = true; 124 125 // ----- passed to IterableQueryResult ----- 126 127 /** The real columns, CMIS name mapped to NXQL. */ 128 protected Map<String, String> realColumns = new LinkedHashMap<>(); 129 130 /** The non-real-columns we'll return as well. */ 131 protected Map<String, ColumnReference> virtualColumns = new LinkedHashMap<String, ColumnReference>(); 132 133 /** 134 * Gets the NXQL from a CMISQL query. 135 */ 136 public String getNXQL(String cmisql, NuxeoCmisService service, Map<String, PropertyDefinition<?>> typeInfo, 137 boolean searchAllVersions) throws QueryParseException { 138 this.typeInfo = typeInfo; 139 boolean searchLatestVersion = !searchAllVersions; 140 TypeManagerImpl typeManager = service.repository.getTypeManager(); 141 coreSession = service.coreSession; 142 143 query = new QueryObject(typeManager); 144 CmisQueryWalker walker = null; 145 try { 146 walker = QueryUtil.getWalker(cmisql); 147 walker.setDoFullTextParse(false); 148 walker.query(query, new AnalyzingWalker()); 149 } catch (RecognitionException e) { 150 String msg; 151 if (walker == null) { 152 msg = e.getMessage(); 153 } else { 154 msg = "Line " + e.line + ":" + e.charPositionInLine + " " 155 + walker.getErrorMessage(e, walker.getTokenNames()); 156 } 157 throw new QueryParseException(msg, e); 158 } 159 if (query.getTypes().size() != 1 && query.getJoinedSecondaryTypes() == null) { 160 throw new QueryParseException("JOINs not supported in query: " + cmisql); 161 } 162 163 fromType = query.getMainFromName(); 164 BaseTypeId fromBaseTypeId = fromType.getBaseTypeId(); 165 166 // now resolve column selectors to actual database columns 167 for (CmisSelector sel : query.getSelectReferences()) { 168 recordSelectSelector(sel); 169 } 170 for (CmisSelector sel : query.getJoinReferences()) { 171 ColumnReference col = ((ColumnReference) sel); 172 if (col.getTypeDefinition().getBaseTypeId() == BaseTypeId.CMIS_SECONDARY) { 173 // ignore reference to ON FACET.cmis:objectId 174 continue; 175 } 176 recordSelector(sel, JOIN); 177 } 178 for (CmisSelector sel : query.getWhereReferences()) { 179 recordSelector(sel, WHERE); 180 } 181 for (SortSpec spec : query.getOrderBys()) { 182 recordSelector(spec.getSelector(), ORDER_BY); 183 } 184 185 addSystemColumns(); 186 187 List<String> whereClauses = new ArrayList<String>(); 188 189 // what to select (result columns) 190 191 String what = StringUtils.join(realColumns.values(), ", "); 192 193 // determine relevant primary types 194 195 String nxqlFrom; 196 if (fromBaseTypeId == CMIS_RELATIONSHIP) { 197 if (fromType.getParentTypeId() == null) { 198 nxqlFrom = NXQL_RELATION; 199 } else { 200 nxqlFrom = fromType.getId(); 201 } 202 } else { 203 nxqlFrom = NXQL_DOCUMENT; 204 List<String> types = new ArrayList<String>(); 205 if (fromType.getParentTypeId() != null) { 206 // don't add abstract root types 207 types.add(fromType.getId()); 208 } 209 LinkedList<TypeDefinitionContainer> typesTodo = new LinkedList<TypeDefinitionContainer>(); 210 typesTodo.addAll(typeManager.getTypeDescendants(fromType.getId(), -1, Boolean.TRUE)); 211 // recurse to get all subtypes 212 TypeDefinitionContainer tc; 213 while ((tc = typesTodo.poll()) != null) { 214 types.add(tc.getTypeDefinition().getId()); 215 typesTodo.addAll(tc.getChildren()); 216 } 217 if (types.isEmpty()) { 218 // shoudn't happen 219 types = Collections.singletonList("__NOSUCHTYPE__"); 220 } 221 // build clause 222 StringBuilder pt = new StringBuilder(); 223 pt.append(NXQL.ECM_PRIMARYTYPE); 224 pt.append(" IN ("); 225 for (Iterator<String> it = types.iterator(); it.hasNext();) { 226 pt.append(QUOTE); 227 pt.append(it.next()); 228 pt.append(QUOTE); 229 if (it.hasNext()) { 230 pt.append(", "); 231 } 232 } 233 pt.append(")"); 234 whereClauses.add(pt.toString()); 235 } 236 237 // lifecycle not deleted filter 238 239 if (skipDeleted) { 240 whereClauses.add(String.format("%s <> '%s'", NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE)); 241 } 242 243 // searchAllVersions filter 244 245 if (searchLatestVersion && fromBaseTypeId == CMIS_DOCUMENT) { 246 whereClauses.add(String.format("%s = 1", NXQL.ECM_ISLATESTVERSION)); 247 } 248 249 // no proxies 250 251 whereClauses.add(String.format("%s = 0", NXQL.ECM_ISPROXY)); 252 253 // WHERE clause 254 255 Tree whereNode = walker.getWherePredicateTree(); 256 boolean distinct = false; 257 if (whereNode != null) { 258 GeneratingWalker generator = new GeneratingWalker(); 259 generator.walkPredicate(whereNode); 260 whereClauses.add(generator.buf.toString()); 261 distinct = generator.distinct; 262 } 263 264 // ORDER BY clause 265 266 List<String> orderbys = new ArrayList<String>(); 267 for (SortSpec spec : query.getOrderBys()) { 268 String orderby; 269 CmisSelector sel = spec.getSelector(); 270 if (sel instanceof ColumnReference) { 271 orderby = (String) sel.getInfo(); 272 } else { 273 orderby = NXQL.ECM_FULLTEXT_SCORE; 274 } 275 if (!spec.ascending) { 276 orderby += " DESC"; 277 } 278 orderbys.add(orderby); 279 } 280 281 // create the whole select 282 283 String where = StringUtils.join(whereClauses, " AND "); 284 String nxql = "SELECT " + (distinct ? "DISTINCT " : "") + what + " FROM " + nxqlFrom + " WHERE " + where; 285 if (!orderbys.isEmpty()) { 286 nxql += " ORDER BY " + StringUtils.join(orderbys, ", "); 287 } 288 // System.err.println("CMIS: " + statement); 289 // System.err.println("NXQL: " + nxql); 290 return nxql; 291 } 292 293 public IterableQueryResult getIterableQueryResult(IterableQueryResult it, NuxeoCmisService service) { 294 return new NXQLtoCMISIterableQueryResult(it, realColumns, virtualColumns, service); 295 } 296 297 protected boolean isFacetsColumn(String name) { 298 return PropertyIds.SECONDARY_OBJECT_TYPE_IDS.equals(name) || NuxeoTypeHelper.NX_FACETS.equals(name); 299 } 300 301 protected void addSystemColumns() { 302 // additional references to cmis:objectId and cmis:objectTypeId 303 for (String propertyId : Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID)) { 304 if (!realColumns.containsKey(propertyId)) { 305 ColumnReference col = new ColumnReference(propertyId); 306 col.setTypeDefinition(propertyId, fromType); 307 recordSelectSelector(col); 308 } 309 } 310 } 311 312 /** 313 * Records a SELECT selector, and associates it to a database column. 314 */ 315 protected void recordSelectSelector(CmisSelector sel) { 316 if (sel instanceof FunctionReference) { 317 FunctionReference fr = (FunctionReference) sel; 318 if (fr.getFunction() != CmisQlFunction.SCORE) { 319 throw new CmisRuntimeException("Unknown function: " + fr.getFunction()); 320 } 321 String key = fr.getAliasName(); 322 if (key == null) { 323 key = "SEARCH_SCORE"; // default, from spec 324 } 325 realColumns.put(key, NXQL.ECM_FULLTEXT_SCORE); 326 if (typeInfo != null) { 327 PropertyDecimalDefinitionImpl pd = new PropertyDecimalDefinitionImpl(); 328 pd.setId(key); 329 pd.setQueryName(key); 330 pd.setCardinality(Cardinality.SINGLE); 331 pd.setDisplayName("Score"); 332 pd.setLocalName("score"); 333 typeInfo.put(key, pd); 334 } 335 } else { // sel instanceof ColumnReference 336 ColumnReference col = (ColumnReference) sel; 337 338 if (col.getPropertyQueryName().equals("*")) { 339 for (PropertyDefinition<?> pd : fromType.getPropertyDefinitions().values()) { 340 String id = pd.getId(); 341 if ((pd.getCardinality() == Cardinality.SINGLE // 342 && Boolean.TRUE.equals(pd.isQueryable())) 343 || id.equals(PropertyIds.BASE_TYPE_ID)) { 344 ColumnReference c = new ColumnReference(null, id); 345 c.setTypeDefinition(id, fromType); 346 recordSelectSelector(c); 347 } 348 } 349 return; 350 } 351 352 String key = col.getPropertyQueryName(); 353 PropertyDefinition<?> pd = col.getPropertyDefinition(); 354 String nxqlCol = getColumn(col); 355 String id = pd.getId(); 356 if (nxqlCol != null && pd.getCardinality() == Cardinality.SINGLE && (Boolean.TRUE.equals(pd.isQueryable()) 357 || id.equals(PropertyIds.BASE_TYPE_ID) || id.equals(PropertyIds.OBJECT_TYPE_ID))) { 358 col.setInfo(nxqlCol); 359 realColumns.put(key, nxqlCol); 360 } else { 361 virtualColumns.put(key, col); 362 } 363 if (typeInfo != null) { 364 typeInfo.put(key, pd); 365 } 366 } 367 } 368 369 protected static final String JOIN = "JOIN"; 370 371 protected static final String WHERE = "WHERE"; 372 373 protected static final String ORDER_BY = "ORDER BY"; 374 375 /** 376 * Records a JOIN / WHERE / ORDER BY selector, and associates it to a database column. 377 */ 378 protected void recordSelector(CmisSelector sel, String clauseType) { 379 if (sel instanceof FunctionReference) { 380 FunctionReference fr = (FunctionReference) sel; 381 if (clauseType != ORDER_BY) { // == ok 382 throw new QueryParseException("Cannot use function in " + clauseType + " clause: " + fr.getFunction()); 383 } 384 // ORDER BY SCORE, nothing further to record 385 return; 386 } 387 ColumnReference col = (ColumnReference) sel; 388 389 // fetch column and associate it to the selector 390 String column = getColumn(col); 391 if (!isFacetsColumn(col.getPropertyId()) && column == null) { 392 throw new QueryParseException("Cannot use column in " + clauseType + " clause: " 393 + col.getPropertyQueryName()); 394 } 395 col.setInfo(column); 396 397 if (clauseType == WHERE && NuxeoTypeHelper.NX_LIFECYCLE_STATE.equals(col.getPropertyId())) { 398 // explicit lifecycle query: do not include the 'deleted' lifecycle 399 // filter 400 skipDeleted = false; 401 } 402 } 403 404 /** 405 * Finds a NXQL column from a CMIS reference. 406 */ 407 protected String getColumn(ColumnReference col) { 408 return getColumn(col.getPropertyId()); 409 } 410 411 /** 412 * Finds a NXQL column from a CMIS reference. 413 */ 414 protected String getColumn(String propertyId) { 415 if (propertyId.startsWith(CMIS_PREFIX) || propertyId.startsWith(NX_PREFIX)) { 416 return getSystemColumn(propertyId); 417 } else { 418 // CMIS property names are identical to NXQL ones 419 // for non-system properties 420 return propertyId; 421 } 422 } 423 424 /** 425 * Finds a NXQL system column from a CMIS system property id. 426 */ 427 protected String getSystemColumn(String propertyId) { 428 switch (propertyId) { 429 case PropertyIds.OBJECT_ID: 430 return NXQL.ECM_UUID; 431 case PropertyIds.PARENT_ID: 432 case NuxeoTypeHelper.NX_PARENT_ID: 433 return NXQL.ECM_PARENTID; 434 case NuxeoTypeHelper.NX_PATH_SEGMENT: 435 return NXQL.ECM_NAME; 436 case NuxeoTypeHelper.NX_POS: 437 return NXQL.ECM_POS; 438 case PropertyIds.OBJECT_TYPE_ID: 439 return NXQL.ECM_PRIMARYTYPE; 440 case PropertyIds.SECONDARY_OBJECT_TYPE_IDS: 441 case NuxeoTypeHelper.NX_FACETS: 442 return NXQL.ECM_MIXINTYPE; 443 case PropertyIds.VERSION_LABEL: 444 return NXQL.ECM_VERSIONLABEL; 445 case PropertyIds.IS_LATEST_MAJOR_VERSION: 446 return NXQL.ECM_ISLATESTMAJORVERSION; 447 case PropertyIds.IS_LATEST_VERSION: 448 return NXQL.ECM_ISLATESTVERSION; 449 case NuxeoTypeHelper.NX_ISVERSION: 450 return NXQL.ECM_ISVERSION; 451 case NuxeoTypeHelper.NX_ISCHECKEDIN: 452 return NXQL.ECM_ISCHECKEDIN; 453 case NuxeoTypeHelper.NX_LIFECYCLE_STATE: 454 return NXQL.ECM_LIFECYCLESTATE; 455 case PropertyIds.NAME: 456 return NXQL_DC_TITLE; 457 case PropertyIds.DESCRIPTION: 458 return NXQL_DC_DESCRIPTION; 459 case PropertyIds.CREATED_BY: 460 return NXQL_DC_CREATOR; 461 case PropertyIds.CREATION_DATE: 462 return NXQL_DC_CREATED; 463 case PropertyIds.LAST_MODIFICATION_DATE: 464 return NXQL_DC_MODIFIED; 465 case PropertyIds.LAST_MODIFIED_BY: 466 return NXQL_DC_LAST_CONTRIBUTOR; 467 case PropertyIds.SOURCE_ID: 468 return NXQL_REL_SOURCE; 469 case PropertyIds.TARGET_ID: 470 return NXQL_REL_TARGET; 471 } 472 return null; 473 } 474 475 protected static String cmisToNxqlFulltextQuery(String statement) { 476 // NXQL syntax has implicit AND 477 statement = statement.replace(" and ", " "); 478 statement = statement.replace(" AND ", " "); 479 return statement; 480 } 481 482 /** 483 * Convert an ORDER BY part from CMISQL to NXQL. 484 * 485 * @since 6.0 486 */ 487 protected String convertOrderBy(String orderBy, TypeManagerImpl typeManager) { 488 List<String> list = new ArrayList<>(1); 489 for (String order : orderBy.split(",")) { 490 order = order.trim(); 491 String lower = order.toLowerCase(); 492 String prop; 493 boolean asc; 494 if (lower.endsWith(SPACE_ASC)) { 495 prop = order.substring(0, order.length() - SPACE_ASC.length()).trim(); 496 asc = true; 497 } else if (lower.endsWith(SPACE_DESC)) { 498 prop = order.substring(0, order.length() - SPACE_DESC.length()).trim(); 499 asc = false; 500 } else { 501 prop = order; 502 asc = true; // default is repository-specific 503 } 504 // assume query name is same as property id 505 String propId = typeManager.getPropertyIdForQueryName(prop); 506 if (propId == null) { 507 throw new CmisInvalidArgumentException("Invalid orderBy: " + orderBy); 508 } 509 String col = getColumn(propId); 510 list.add(asc ? col : (col + " DESC")); 511 } 512 return StringUtils.join(list, ", "); 513 } 514 515 /** 516 * Walker of the WHERE clause that doesn't parse fulltext expressions. 517 */ 518 public class AnalyzingWalker extends AbstractPredicateWalker { 519 520 public boolean hasContains; 521 522 @Override 523 public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) { 524 if (hasContains) { 525 throw new QueryParseException("At most one CONTAINS() is allowed"); 526 } 527 hasContains = true; 528 return null; 529 } 530 } 531 532 /** 533 * Walker of the WHERE clause that generates NXQL. 534 */ 535 public class GeneratingWalker extends AbstractPredicateWalker { 536 537 public static final String NX_FULLTEXT_INDEX_PREFIX = "nx:"; 538 539 public StringBuilder buf = new StringBuilder(); 540 541 public boolean distinct; 542 543 @Override 544 public Boolean walkNot(Tree opNode, Tree node) { 545 buf.append("NOT "); 546 walkPredicate(node); 547 return null; 548 } 549 550 @Override 551 public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) { 552 buf.append("("); 553 walkPredicate(leftNode); 554 buf.append(" AND "); 555 walkPredicate(rightNode); 556 buf.append(")"); 557 return null; 558 } 559 560 @Override 561 public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) { 562 buf.append("("); 563 walkPredicate(leftNode); 564 buf.append(" OR "); 565 walkPredicate(rightNode); 566 buf.append(")"); 567 return null; 568 } 569 570 @Override 571 public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) { 572 walkExpr(leftNode); 573 buf.append(" = "); 574 walkExpr(rightNode); 575 return null; 576 } 577 578 @Override 579 public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) { 580 walkExpr(leftNode); 581 buf.append(" <> "); 582 walkExpr(rightNode); 583 return null; 584 } 585 586 @Override 587 public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) { 588 walkExpr(leftNode); 589 buf.append(" > "); 590 walkExpr(rightNode); 591 return null; 592 } 593 594 @Override 595 public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) { 596 walkExpr(leftNode); 597 buf.append(" >= "); 598 walkExpr(rightNode); 599 return null; 600 } 601 602 @Override 603 public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) { 604 walkExpr(leftNode); 605 buf.append(" < "); 606 walkExpr(rightNode); 607 return null; 608 } 609 610 @Override 611 public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) { 612 walkExpr(leftNode); 613 buf.append(" <= "); 614 walkExpr(rightNode); 615 return null; 616 } 617 618 @Override 619 public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) { 620 walkExpr(colNode); 621 buf.append(" IN "); 622 walkExpr(listNode); 623 return null; 624 } 625 626 @Override 627 public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) { 628 walkExpr(colNode); 629 buf.append(" NOT IN "); 630 walkExpr(listNode); 631 return null; 632 } 633 634 @Override 635 public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) { 636 walkAny(colNode, "IN", listNode); 637 return null; 638 } 639 640 @Override 641 public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) { 642 walkAny(colNode, "NOT IN", listNode); 643 return null; 644 } 645 646 @Override 647 public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) { 648 // note that argument order is reversed 649 walkAny(colNode, "=", literalNode); 650 return null; 651 } 652 653 protected void walkAny(Tree colNode, String op, Tree exprNode) { 654 ColumnReference col = getColumnReference(colNode); 655 if (col.getPropertyDefinition().getCardinality() != Cardinality.MULTI) { 656 throw new QueryParseException("Cannot use " + op + " ANY with single-valued property: " 657 + col.getPropertyQueryName()); 658 } 659 String nxqlCol = (String) col.getInfo(); 660 buf.append(nxqlCol); 661 if (!NXQL.ECM_MIXINTYPE.equals(nxqlCol)) { 662 buf.append("/*"); 663 } 664 distinct = true; 665 buf.append(' '); 666 buf.append(op); 667 buf.append(' '); 668 walkExpr(exprNode); 669 } 670 671 @Override 672 public Boolean walkIsNull(Tree opNode, Tree colNode) { 673 return walkIsNullOrIsNotNull(colNode, true); 674 } 675 676 @Override 677 public Boolean walkIsNotNull(Tree opNode, Tree colNode) { 678 return walkIsNullOrIsNotNull(colNode, false); 679 } 680 681 protected Boolean walkIsNullOrIsNotNull(Tree colNode, boolean isNull) { 682 ColumnReference col = getColumnReference(colNode); 683 boolean multi = col.getPropertyDefinition().getCardinality() == Cardinality.MULTI; 684 walkExpr(colNode); 685 if (multi) { 686 buf.append("/*"); 687 distinct = true; 688 } 689 buf.append(isNull ? " IS NULL" : " IS NOT NULL"); 690 return null; 691 } 692 693 @Override 694 public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) { 695 walkExpr(colNode); 696 buf.append(" LIKE "); 697 walkExpr(stringNode); 698 return null; 699 } 700 701 @Override 702 public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) { 703 walkExpr(colNode); 704 buf.append(" NOT LIKE "); 705 walkExpr(stringNode); 706 return null; 707 } 708 709 @Override 710 public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) { 711 String statement = (String) super.walkString(queryNode); 712 String indexName = NXQL.ECM_FULLTEXT; 713 // micro parsing of the fulltext statement to perform fulltext 714 // search on a non default index 715 if (statement.startsWith(NX_FULLTEXT_INDEX_PREFIX)) { 716 statement = statement.substring(NX_FULLTEXT_INDEX_PREFIX.length()); 717 int firstColumnIdx = statement.indexOf(':'); 718 if (firstColumnIdx > 0 && firstColumnIdx < statement.length() - 1) { 719 indexName += '_' + statement.substring(0, firstColumnIdx); 720 statement = statement.substring(firstColumnIdx + 1); 721 } else { 722 log.warn(String.format("fail to microparse custom fulltext index:" + " fallback to '%s'", indexName)); 723 } 724 } 725 // CMIS syntax to NXQL syntax 726 statement = cmisToNxqlFulltextQuery(statement); 727 buf.append(indexName); 728 buf.append(" = "); 729 buf.append(NXQL.escapeString(statement)); 730 return null; 731 } 732 733 @Override 734 public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) { 735 String id = (String) super.walkString(paramNode); 736 buf.append(NXQL.ECM_PARENTID); 737 buf.append(" = "); 738 buf.append(NXQL.escapeString(id)); 739 return null; 740 } 741 742 @Override 743 public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) { 744 String id = (String) super.walkString(paramNode); 745 // don't use ecm:ancestorId because the Elasticsearch converter doesn't understand it 746 // buf.append(NXQL.ECM_ANCESTORID); 747 // buf.append(" = "); 748 // buf.append(NXQL.escapeString(id)); 749 String path; 750 DocumentRef docRef = new IdRef(id); 751 if (coreSession.exists(docRef)) { 752 path = coreSession.getDocument(docRef).getPathAsString(); 753 } else { 754 // TODO better removal 755 path = "/__NOSUCHPATH__"; 756 } 757 buf.append(NXQL.ECM_PATH); 758 buf.append(" STARTSWITH "); 759 buf.append(NXQL.escapeString(path)); 760 return null; 761 } 762 763 @Override 764 public Object walkList(Tree node) { 765 buf.append("("); 766 for (int i = 0; i < node.getChildCount(); i++) { 767 if (i != 0) { 768 buf.append(", "); 769 } 770 Tree child = node.getChild(i); 771 walkExpr(child); 772 } 773 buf.append(")"); 774 return null; 775 } 776 777 @Override 778 public Object walkBoolean(Tree node) { 779 Object value = super.walkBoolean(node); 780 buf.append(Boolean.FALSE.equals(value) ? "0" : "1"); 781 return null; 782 } 783 784 @Override 785 public Object walkNumber(Tree node) { 786 // Double or Long 787 Number value = (Number) super.walkNumber(node); 788 buf.append(value.toString()); 789 return null; 790 } 791 792 @Override 793 public Object walkString(Tree node) { 794 String value = (String) super.walkString(node); 795 buf.append(NXQL.escapeString(value)); 796 return null; 797 } 798 799 @Override 800 public Object walkTimestamp(Tree node) { 801 Calendar value = (Calendar) super.walkTimestamp(node); 802 buf.append("TIMESTAMP "); 803 buf.append(QUOTE); 804 buf.append(ISO_DATE_TIME_FORMAT.print(LocalDateTime.fromCalendarFields(value))); 805 buf.append(QUOTE); 806 return null; 807 } 808 809 @Override 810 public Object walkCol(Tree node) { 811 String nxqlCol = (String) getColumnReference(node).getInfo(); 812 buf.append(nxqlCol); 813 return null; 814 } 815 816 protected ColumnReference getColumnReference(Tree node) { 817 CmisSelector sel = query.getColumnReference(Integer.valueOf(node.getTokenStartIndex())); 818 if (sel instanceof ColumnReference) { 819 return (ColumnReference) sel; 820 } else { 821 throw new QueryParseException("Cannot use column in WHERE clause: " + sel.getName()); 822 } 823 } 824 } 825 826 /** 827 * IterableQueryResult wrapping the one from the NXQL query to turn values into CMIS ones. 828 */ 829 // static to avoid keeping the whole QueryMaker in the returned object 830 public static class NXQLtoCMISIterableQueryResult implements IterableQueryResult, 831 Iterator<Map<String, Serializable>> { 832 833 protected IterableQueryResult it; 834 835 protected Iterator<Map<String, Serializable>> iter; 836 837 protected Map<String, String> realColumns; 838 839 protected Map<String, ColumnReference> virtualColumns; 840 841 protected NuxeoCmisService service; 842 843 public NXQLtoCMISIterableQueryResult(IterableQueryResult it, Map<String, String> realColumns, 844 Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) { 845 this.it = it; 846 iter = it.iterator(); 847 this.realColumns = realColumns; 848 this.virtualColumns = virtualColumns; 849 this.service = service; 850 } 851 852 @Override 853 public Iterator<Map<String, Serializable>> iterator() { 854 return this; 855 } 856 857 @Override 858 public void close() { 859 it.close(); 860 } 861 862 @Override 863 public boolean isLife() { 864 return it.isLife(); 865 } 866 867 @Override 868 public long size() { 869 return it.size(); 870 } 871 872 @Override 873 public long pos() { 874 return it.pos(); 875 } 876 877 @Override 878 public void skipTo(long pos) { 879 it.skipTo(pos); 880 } 881 882 @Override 883 public boolean hasNext() { 884 return iter.hasNext(); 885 } 886 887 @Override 888 public void remove() { 889 throw new UnsupportedOperationException(); 890 } 891 892 @Override 893 public Map<String, Serializable> next() { 894 // map of NXQL to value 895 Map<String, Serializable> nxqlMap = iter.next(); 896 897 // find the CMIS keys and values 898 Map<String, Serializable> cmisMap = new HashMap<>(); 899 for (Entry<String, String> en : realColumns.entrySet()) { 900 String cmisCol = en.getKey(); 901 String nxqlCol = en.getValue(); 902 Serializable value = nxqlMap.get(nxqlCol); 903 // type conversion to CMIS values 904 if (value instanceof Long) { 905 value = BigInteger.valueOf(((Long) value).longValue()); 906 } else if (value instanceof Integer) { 907 value = BigInteger.valueOf(((Integer) value).intValue()); 908 } else if (value instanceof Double) { 909 value = BigDecimal.valueOf(((Double) value).doubleValue()); 910 } else if (value == null) { 911 // special handling of some columns where NULL means FALSE 912 if (NULL_IS_FALSE_COLUMNS.contains(nxqlCol)) { 913 value = Boolean.FALSE; 914 } 915 } 916 cmisMap.put(cmisCol, value); 917 } 918 919 // virtual values 920 // map to store actual data for each qualifier 921 Map<String, NuxeoObjectData> datas = null; 922 for (Entry<String, ColumnReference> vc : virtualColumns.entrySet()) { 923 String key = vc.getKey(); 924 ColumnReference col = vc.getValue(); 925 String qual = col.getQualifier(); 926 if (col.getPropertyId().equals(PropertyIds.BASE_TYPE_ID)) { 927 // special case, no need to get full Nuxeo Document 928 String typeId = (String) cmisMap.get(PropertyIds.OBJECT_TYPE_ID); 929 if (typeId == null) { 930 throw new NullPointerException(); 931 } 932 TypeDefinitionContainer type = service.repository.getTypeManager().getTypeById(typeId); 933 String baseTypeId = type.getTypeDefinition().getBaseTypeId().value(); 934 cmisMap.put(key, baseTypeId); 935 continue; 936 } 937 if (datas == null) { 938 datas = new HashMap<String, NuxeoObjectData>(2); 939 } 940 NuxeoObjectData data = datas.get(qual); 941 if (data == null) { 942 // find main id for this qualifier in the result set 943 // (main id always included in joins) 944 // TODO check what happens if cmis:objectId is aliased 945 String id = (String) cmisMap.get(PropertyIds.OBJECT_ID); 946 try { 947 // reentrant call to the same session, but the MapMaker 948 // is only called from the IterableQueryResult in 949 // queryAndFetch which manipulates no session state 950 // TODO constructing the DocumentModel (in 951 // NuxeoObjectData) is expensive, try to get value 952 // directly 953 data = (NuxeoObjectData) service.getObject(service.getNuxeoRepository().getId(), id, null, 954 null, null, null, null, null, null); 955 } catch (CmisRuntimeException e) { 956 log.error("Cannot get document: " + id, e); 957 } 958 datas.put(qual, data); 959 } 960 Serializable v; 961 if (data == null) { 962 // could not fetch 963 v = null; 964 } else { 965 NuxeoPropertyDataBase<?> pd = (NuxeoPropertyDataBase<?>) data.getProperty(col.getPropertyId()); 966 if (pd == null) { 967 v = null; 968 } else { 969 if (pd.getPropertyDefinition().getCardinality() == Cardinality.SINGLE) { 970 v = (Serializable) pd.getFirstValue(); 971 } else { 972 v = (Serializable) pd.getValues(); 973 } 974 } 975 } 976 cmisMap.put(key, v); 977 } 978 979 return cmisMap; 980 } 981 } 982 983}