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