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