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