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