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