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