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