001/* 002 * (C) Copyright 2014-2018 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 * Tiry 018 * bdelbosc 019 */ 020package org.nuxeo.elasticsearch.query; 021 022import static org.nuxeo.elasticsearch.ElasticSearchConstants.ES_SCORE_FIELD; 023import static org.nuxeo.elasticsearch.ElasticSearchConstants.FULLTEXT_FIELD; 024 025import java.io.StringReader; 026import java.time.ZonedDateTime; 027import java.util.ArrayList; 028import java.util.Calendar; 029import java.util.Collection; 030import java.util.GregorianCalendar; 031import java.util.Iterator; 032import java.util.LinkedHashMap; 033import java.util.LinkedList; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037 038import org.apache.commons.lang3.ArrayUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.elasticsearch.index.query.BoolQueryBuilder; 041import org.elasticsearch.index.query.MatchPhrasePrefixQueryBuilder; 042import org.elasticsearch.index.query.MoreLikeThisQueryBuilder; 043import org.elasticsearch.index.query.QueryBuilder; 044import org.elasticsearch.index.query.QueryBuilders; 045import org.elasticsearch.index.query.SimpleQueryStringBuilder; 046import org.nuxeo.ecm.core.api.CoreSession; 047import org.nuxeo.ecm.core.api.DocumentModel; 048import org.nuxeo.ecm.core.api.DocumentNotFoundException; 049import org.nuxeo.ecm.core.api.IdRef; 050import org.nuxeo.ecm.core.api.LifeCycleConstants; 051import org.nuxeo.ecm.core.api.SortInfo; 052import org.nuxeo.ecm.core.api.trash.TrashService; 053import org.nuxeo.ecm.core.api.trash.TrashService.Feature; 054import org.nuxeo.ecm.core.query.QueryParseException; 055import org.nuxeo.ecm.core.query.sql.NXQL; 056import org.nuxeo.ecm.core.query.sql.SQLQueryParser; 057import org.nuxeo.ecm.core.query.sql.model.DefaultQueryVisitor; 058import org.nuxeo.ecm.core.query.sql.model.EsHint; 059import org.nuxeo.ecm.core.query.sql.model.Expression; 060import org.nuxeo.ecm.core.query.sql.model.FromClause; 061import org.nuxeo.ecm.core.query.sql.model.FromList; 062import org.nuxeo.ecm.core.query.sql.model.Function; 063import org.nuxeo.ecm.core.query.sql.model.Literal; 064import org.nuxeo.ecm.core.query.sql.model.LiteralList; 065import org.nuxeo.ecm.core.query.sql.model.MultiExpression; 066import org.nuxeo.ecm.core.query.sql.model.Operand; 067import org.nuxeo.ecm.core.query.sql.model.Operator; 068import org.nuxeo.ecm.core.query.sql.model.OrderByExpr; 069import org.nuxeo.ecm.core.query.sql.model.Predicate; 070import org.nuxeo.ecm.core.query.sql.model.Reference; 071import org.nuxeo.ecm.core.query.sql.model.SQLQuery; 072import org.nuxeo.ecm.core.query.sql.model.SelectClause; 073import org.nuxeo.ecm.core.query.sql.model.StringLiteral; 074import org.nuxeo.ecm.core.schema.SchemaManager; 075import org.nuxeo.ecm.core.schema.types.Field; 076import org.nuxeo.ecm.core.schema.types.Type; 077import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 078import org.nuxeo.ecm.core.schema.utils.DateParser; 079import org.nuxeo.ecm.core.security.SecurityService; 080import org.nuxeo.ecm.core.storage.sql.jdbc.NXQLQueryMaker; 081import org.nuxeo.elasticsearch.api.ElasticSearchAdmin; 082import org.nuxeo.elasticsearch.hint.MoreLikeThisESHintQueryBuilder; 083import org.nuxeo.runtime.api.Framework; 084 085/** 086 * Helper class that holds the conversion logic. Conversion is based on the existing NXQL Parser, we are just using a 087 * visitor to build the ES request. 088 */ 089public final class NxqlQueryConverter { 090 091 protected static final String SELECT_ALL = "SELECT * FROM Document"; 092 093 protected static final String SELECT_ALL_WHERE = "SELECT * FROM Document WHERE "; 094 095 protected static final String SIMPLE_QUERY_PREFIX = "es: "; 096 097 /** 098 * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#MORE_LIKE_THIS_MIN_TERM_FREQ} instead. 099 */ 100 @Deprecated 101 protected static final int MORE_LIKE_THIS_MIN_TERM_FREQ = MoreLikeThisESHintQueryBuilder.MORE_LIKE_THIS_MIN_TERM_FREQ; 102 103 /** 104 * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#MORE_LIKE_THIS_MIN_DOC_FREQ} instead. 105 */ 106 @Deprecated 107 protected static final int MORE_LIKE_THIS_MIN_DOC_FREQ = MoreLikeThisESHintQueryBuilder.MORE_LIKE_THIS_MIN_DOC_FREQ; 108 109 /** 110 * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#MORE_LIKE_THIS_MAX_QUERY_TERMS} instead. 111 */ 112 @Deprecated 113 protected static final int MORE_LIKE_THIS_MAX_QUERY_TERMS = MoreLikeThisESHintQueryBuilder.MORE_LIKE_THIS_MAX_QUERY_TERMS; 114 115 private NxqlQueryConverter() { 116 } 117 118 public static QueryBuilder toESQueryBuilder(final String nxql) { 119 return toESQueryBuilder(nxql, null); 120 } 121 122 public static QueryBuilder toESQueryBuilder(final String nxql, final CoreSession session) { 123 final LinkedList<ExpressionBuilder> builders = new LinkedList<>(); 124 SQLQuery nxqlQuery = getSqlQuery(nxql); 125 if (session != null) { 126 nxqlQuery = addSecurityPolicy(session, nxqlQuery); 127 } 128 final ExpressionBuilder ret = new ExpressionBuilder(null); 129 builders.add(ret); 130 final ArrayList<String> fromList = new ArrayList<>(); 131 nxqlQuery.accept(new DefaultQueryVisitor() { 132 133 @Override 134 public void visitFromClause(FromClause node) { 135 FromList elements = node.elements; 136 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 137 138 for (String type : elements.values()) { 139 if (NXQLQueryMaker.TYPE_DOCUMENT.equalsIgnoreCase(type)) { 140 // From Document means all doc types 141 fromList.clear(); 142 return; 143 } 144 Set<String> types = schemaManager.getDocumentTypeNamesExtending(type); 145 if (types != null) { 146 fromList.addAll(types); 147 } 148 } 149 } 150 151 @Override 152 public void visitMultiExpression(MultiExpression node) { 153 for (Iterator<Predicate> it = node.predicates.iterator(); it.hasNext();) { 154 it.next().accept(this); 155 if (it.hasNext()) { 156 node.operator.accept(this); 157 } 158 } 159 } 160 161 @Override 162 public void visitSelectClause(SelectClause node) { 163 // NOP 164 } 165 166 @Override 167 public void visitExpression(Expression node) { 168 Operator op = node.operator; 169 if (op == Operator.AND || op == Operator.OR || op == Operator.NOT) { 170 builders.add(new ExpressionBuilder(op.toString())); 171 super.visitExpression(node); 172 ExpressionBuilder expr = builders.removeLast(); 173 if (!builders.isEmpty()) { 174 builders.getLast().merge(expr); 175 } 176 } else { 177 Reference ref = node.lvalue instanceof Reference ? (Reference) node.lvalue : null; 178 String name = ref != null ? ref.name : node.lvalue.toString(); 179 String value = null; 180 if (node.rvalue instanceof Literal) { 181 value = ((Literal) node.rvalue).asString(); 182 } else if (node.rvalue instanceof Function) { 183 Function function = (Function) node.rvalue; 184 String func = function.name; 185 if (NXQL.NOW_FUNCTION.equalsIgnoreCase(func)) { 186 String periodAndDurationText; 187 if (function.args == null || function.args.size() != 1) { 188 periodAndDurationText = null; 189 } else { 190 periodAndDurationText = ((StringLiteral) function.args.get(0)).value; 191 } 192 ZonedDateTime dateTime = NXQL.nowPlusPeriodAndDuration(periodAndDurationText); 193 Calendar calendar = GregorianCalendar.from(dateTime); 194 value = DateParser.formatW3CDateTime(calendar); 195 } else { 196 throw new IllegalArgumentException("Unknown function: " + func); 197 } 198 } else if (node.rvalue != null) { 199 value = node.rvalue.toString(); 200 } 201 Object[] values = null; 202 if (node.rvalue instanceof LiteralList) { 203 LiteralList items = (LiteralList) node.rvalue; 204 values = new Object[items.size()]; 205 int i = 0; 206 for (Literal item : items) { 207 values[i++] = item.asString(); 208 } 209 } 210 // add expression to the last builder 211 EsHint hint = (ref != null) ? ref.esHint : null; 212 builders.getLast() 213 .add(makeQueryFromSimpleExpression(op.toString(), name, value, values, hint, session)); 214 } 215 } 216 }); 217 QueryBuilder queryBuilder = ret.get(); 218 if (!fromList.isEmpty()) { 219 return QueryBuilders.boolQuery() 220 .must(queryBuilder) 221 .filter(makeQueryFromSimpleExpression("IN", NXQL.ECM_PRIMARYTYPE, null, 222 fromList.toArray(), null, null).filter); 223 } 224 return queryBuilder; 225 } 226 227 protected static SQLQuery getSqlQuery(String nxql) { 228 String query = completeQueryWithSelect(nxql); 229 SQLQuery nxqlQuery; 230 try { 231 nxqlQuery = SQLQueryParser.parse(new StringReader(query)); 232 } catch (QueryParseException e) { 233 e.addInfo("Query: " + query); 234 throw e; 235 } 236 return nxqlQuery; 237 } 238 239 protected static SQLQuery addSecurityPolicy(CoreSession session, SQLQuery query) { 240 Collection<SQLQuery.Transformer> transformers = Framework.getService(SecurityService.class) 241 .getPoliciesQueryTransformers( 242 session.getRepositoryName()); 243 for (SQLQuery.Transformer trans : transformers) { 244 query = trans.transform(session.getPrincipal(), query); 245 } 246 return query; 247 } 248 249 protected static String completeQueryWithSelect(String nxql) { 250 String query = (nxql == null) ? "" : nxql.trim(); 251 if (query.isEmpty()) { 252 query = SELECT_ALL; 253 } else if (!query.toLowerCase().startsWith("select ")) { 254 query = SELECT_ALL_WHERE + nxql; 255 } 256 return query; 257 } 258 259 public static QueryAndFilter makeQueryFromSimpleExpression(String op, String nxqlName, Object value, 260 Object[] values, EsHint hint, CoreSession session) { 261 QueryBuilder query = null; 262 QueryBuilder filter = null; 263 String name = getFieldName(nxqlName, hint); 264 if (hint != null && hint.operator != null) { 265 if (ArrayUtils.isNotEmpty(values)) { 266 filter = makeHintQuery(name, values, hint); 267 } else { 268 query = makeHintQuery(name, value, hint); 269 } 270 } else if (nxqlName.startsWith(NXQL.ECM_FULLTEXT) && ("=".equals(op) || "!=".equals(op) || "<>".equals(op) 271 || "LIKE".equals(op) || "NOT LIKE".equals(op))) { 272 query = makeFulltextQuery(nxqlName, (String) value, hint); 273 if ("!=".equals(op) || "<>".equals(op) || "NOT LIKE".equals(op)) { 274 filter = QueryBuilders.boolQuery().mustNot(query); 275 query = null; 276 } 277 } else if (nxqlName.startsWith(NXQL.ECM_ANCESTORID)) { 278 filter = makeAncestorIdFilter((String) value, session); 279 if ("!=".equals(op) || "<>".equals(op)) { 280 filter = QueryBuilders.boolQuery().mustNot(filter); 281 } 282 } else if (nxqlName.equals(NXQL.ECM_ISTRASHED)) { 283 filter = makeTrashedFilter(op, name, (String) value); 284 285 } else 286 switch (op) { 287 case "=": 288 filter = QueryBuilders.termQuery(name, checkBoolValue(nxqlName, value)); 289 break; 290 case "<>": 291 case "!=": 292 filter = QueryBuilders.boolQuery() 293 .mustNot(QueryBuilders.termQuery(name, checkBoolValue(nxqlName, value))); 294 break; 295 case ">": 296 filter = QueryBuilders.rangeQuery(name).gt(value); 297 break; 298 case "<": 299 filter = QueryBuilders.rangeQuery(name).lt(value); 300 break; 301 case ">=": 302 filter = QueryBuilders.rangeQuery(name).gte(value); 303 break; 304 case "<=": 305 filter = QueryBuilders.rangeQuery(name).lte(value); 306 break; 307 case "BETWEEN": 308 case "NOT BETWEEN": 309 filter = QueryBuilders.rangeQuery(name).from(values[0]).to(values[1]); 310 if (op.startsWith("NOT")) { 311 filter = QueryBuilders.boolQuery().mustNot(filter); 312 } 313 break; 314 case "IN": 315 case "NOT IN": 316 filter = QueryBuilders.termsQuery(name, values); 317 if (op.startsWith("NOT")) { 318 filter = QueryBuilders.boolQuery().mustNot(filter); 319 } 320 break; 321 case "IS NULL": 322 filter = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(name)); 323 break; 324 case "IS NOT NULL": 325 filter = QueryBuilders.existsQuery(name); 326 break; 327 case "LIKE": 328 case "ILIKE": 329 case "NOT LIKE": 330 case "NOT ILIKE": 331 query = makeLikeQuery(op, name, (String) value, hint); 332 if (op.startsWith("NOT")) { 333 filter = QueryBuilders.boolQuery().mustNot(query); 334 query = null; 335 } 336 break; 337 case "STARTSWITH": 338 filter = makeStartsWithQuery(name, value); 339 break; 340 default: 341 throw new UnsupportedOperationException("Operator: '" + op + "' is unknown"); 342 } 343 return new QueryAndFilter(query, filter); 344 } 345 346 protected static Object checkBoolValue(String nxqlName, Object value) { 347 if (!"0".equals(value) && !"1".equals(value)) { 348 return value; 349 } 350 switch (nxqlName) { 351 case NXQL.ECM_ISPROXY: 352 case NXQL.ECM_ISCHECKEDIN: 353 case NXQL.ECM_ISTRASHED: 354 case NXQL.ECM_ISVERSION: 355 case NXQL.ECM_ISVERSION_OLD: 356 case NXQL.ECM_ISRECORD: 357 case NXQL.ECM_HASLEGALHOLD: 358 case NXQL.ECM_ISLATESTMAJORVERSION: 359 case NXQL.ECM_ISLATESTVERSION: 360 break; 361 default: 362 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 363 Field field = schemaManager.getField(nxqlName); 364 if (field == null || !BooleanType.ID.equals(field.getType().getName())) { 365 return value; 366 } 367 } 368 return "0".equals(value) ? "false" : "true"; 369 } 370 371 protected static QueryBuilder makeTrashedFilter(String op, String name, String value) { 372 boolean equalsDeleted; 373 switch (op) { 374 case "=": 375 equalsDeleted = true; 376 break; 377 case "<>": 378 case "!=": 379 equalsDeleted = false; 380 break; 381 default: 382 throw new IllegalArgumentException(NXQL.ECM_ISTRASHED + " requires = or <> operator"); 383 } 384 if ("0".equals(value)) { 385 equalsDeleted = !equalsDeleted; 386 } else if ("1".equals(value)) { 387 // equalsDeleted unchanged 388 } else { 389 throw new IllegalArgumentException(NXQL.ECM_ISTRASHED + " requires literal 0 or 1 as right argument"); 390 } 391 TrashService trashService = Framework.getService(TrashService.class); 392 QueryBuilder filter = null; 393 if (trashService.hasFeature(Feature.TRASHED_STATE_IS_DEDUCED_FROM_LIFECYCLE)) { 394 filter = QueryBuilders.termQuery(NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE); 395 } else if (trashService.hasFeature(Feature.TRASHED_STATE_IN_MIGRATION)) { 396 filter = QueryBuilders.boolQuery() 397 .should(QueryBuilders.termQuery(NXQL.ECM_LIFECYCLESTATE, 398 LifeCycleConstants.DELETED_STATE)) 399 .should(QueryBuilders.termQuery(name, true)); 400 } else if (trashService.hasFeature(Feature.TRASHED_STATE_IS_DEDICATED_PROPERTY)) { 401 filter = QueryBuilders.termQuery(name, true); 402 } 403 if (!equalsDeleted) { 404 filter = QueryBuilders.boolQuery().mustNot(filter); 405 } 406 return filter; 407 } 408 409 protected static QueryBuilder makeHintQuery(String name, Object value, EsHint hint) { 410 return Framework.getService(ElasticSearchAdmin.class) 411 .getHintByOperator(hint.operator) 412 .orElseThrow(() -> new UnsupportedOperationException( 413 String.format("Operator: %s is unknown", hint.operator))) 414 .make(hint, name, value); 415 } 416 417 /** 418 * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#getItems(Object)} instead. 419 */ 420 @Deprecated 421 protected static MoreLikeThisQueryBuilder.Item[] getItems(Object value) { 422 return MoreLikeThisESHintQueryBuilder.getItems(value); 423 } 424 425 public static QueryBuilder makeStartsWithQuery(String name, Object value) { 426 QueryBuilder filter; 427 String indexName = name + ".children"; 428 if ("/".equals(value)) { 429 if (NXQL.ECM_PATH.equals(name)) { 430 // any non orphan|place-less document must have a path starting with "/" 431 filter = QueryBuilders.existsQuery(NXQL.ECM_PARENTID); 432 } else { 433 // match any document with a populated field 434 filter = QueryBuilders.existsQuery(indexName); 435 } 436 } else { 437 String v = String.valueOf(value); 438 if (v.endsWith("/")) { 439 v = v.replaceAll("/$", ""); 440 } 441 if (NXQL.ECM_PATH.equals(name)) { 442 // we don't want to return the parent when searching on ecm:path, see NXP-18955 443 filter = QueryBuilders.boolQuery() 444 .must(QueryBuilders.termQuery(indexName, v)) 445 .mustNot(QueryBuilders.termQuery(name, value)); 446 } else { 447 filter = QueryBuilders.termQuery(indexName, v); 448 } 449 } 450 return filter; 451 } 452 453 protected static QueryBuilder makeAncestorIdFilter(String value, CoreSession session) { 454 String path; 455 if (session == null) { 456 return QueryBuilders.existsQuery("ancestorid-without-session"); 457 } else { 458 try { 459 DocumentModel doc = session.getDocument(new IdRef(value)); 460 path = doc.getPathAsString(); 461 } catch (DocumentNotFoundException e) { 462 return QueryBuilders.existsQuery("ancestorid-not-found"); 463 } 464 } 465 return makeStartsWithQuery(NXQL.ECM_PATH, path); 466 } 467 468 protected static QueryBuilder makeLikeQuery(String op, String name, String value, EsHint hint) { 469 String fieldName = name; 470 if (op.contains("ILIKE")) { 471 // ILIKE will work only with a correct mapping 472 value = value.toLowerCase(); 473 fieldName = name + ".lowercase"; 474 } 475 if (hint != null && hint.index != null) { 476 fieldName = hint.index; 477 } 478 // convert the value to a wildcard query 479 String wildcard = likeToWildcard(value); 480 // use match phrase prefix when possible 481 if (StringUtils.countMatches(wildcard, "*") == 1 && wildcard.endsWith("*") && !wildcard.contains("?") 482 && !wildcard.contains("\\")) { 483 MatchPhrasePrefixQueryBuilder query = QueryBuilders.matchPhrasePrefixQuery(fieldName, 484 wildcard.replace("*", "")); 485 if (hint != null && hint.analyzer != null) { 486 query.analyzer(hint.analyzer); 487 } 488 return query; 489 } 490 return QueryBuilders.wildcardQuery(fieldName, wildcard); 491 } 492 493 /** 494 * Turns a NXQL LIKE pattern into a wildcard for WildcardQuery. 495 * <p> 496 * % and _ are standard wildcards, and \ escapes them. 497 * 498 * @since 7.4 499 */ 500 protected static String likeToWildcard(String like) { 501 StringBuilder wildcard = new StringBuilder(); 502 char[] chars = like.toCharArray(); 503 boolean escape = false; 504 for (char c : chars) { 505 boolean escapeNext = false; 506 switch (c) { 507 case '?': 508 wildcard.append("\\?"); 509 break; 510 case '*': // compat, * = % in NXQL (for some backends) 511 case '%': 512 if (escape) { 513 wildcard.append(c); 514 } else { 515 wildcard.append("*"); 516 } 517 break; 518 case '_': 519 if (escape) { 520 wildcard.append(c); 521 } else { 522 wildcard.append("?"); 523 } 524 break; 525 case '\\': 526 if (escape) { 527 wildcard.append("\\\\"); 528 } else { 529 escapeNext = true; 530 } 531 break; 532 default: 533 wildcard.append(c); 534 break; 535 } 536 escape = escapeNext; 537 } 538 if (escape) { 539 // invalid string terminated by escape character, ignore 540 } 541 return wildcard.toString(); 542 } 543 544 protected static QueryBuilder makeFulltextQuery(String nxqlName, String value, EsHint hint) { 545 String name = nxqlName.replace(NXQL.ECM_FULLTEXT, ""); 546 if (name.startsWith(".")) { 547 name = name.substring(1) + ".fulltext"; 548 } else { 549 // map ecm:fulltext_someindex to default 550 name = FULLTEXT_FIELD; 551 } 552 String queryString = value; 553 org.elasticsearch.index.query.Operator defaultOperator; 554 if (queryString.startsWith(SIMPLE_QUERY_PREFIX)) { 555 // elasticsearch-specific syntax 556 queryString = queryString.substring(SIMPLE_QUERY_PREFIX.length()); 557 defaultOperator = org.elasticsearch.index.query.Operator.OR; 558 } else { 559 queryString = translateFulltextQuery(queryString); 560 defaultOperator = org.elasticsearch.index.query.Operator.AND; 561 } 562 String analyzer = (hint != null && hint.analyzer != null) ? hint.analyzer : "fulltext"; 563 SimpleQueryStringBuilder query = QueryBuilders.simpleQueryStringQuery(queryString) 564 .defaultOperator(defaultOperator) 565 .analyzer(analyzer); 566 if (hint != null && hint.index != null) { 567 for (EsHint.FieldHint fieldHint : hint.getIndex()) { 568 query.field(fieldHint.getField(), fieldHint.getBoost()); 569 } 570 } else { 571 query.field(name); 572 } 573 return query; 574 } 575 576 protected static String getFieldName(String name, EsHint hint) { 577 if (hint != null && hint.index != null) { 578 return hint.index; 579 } 580 // compat 581 if (NXQL.ECM_ISVERSION_OLD.equals(name)) { 582 name = NXQL.ECM_ISVERSION; 583 } 584 // complex field 585 name = name.replace("/*", ""); 586 name = name.replace("/", "."); 587 return name; 588 } 589 590 public static List<SortInfo> getSortInfo(String nxql) { 591 final List<SortInfo> sortInfos = new ArrayList<>(); 592 SQLQuery nxqlQuery = getSqlQuery(nxql); 593 nxqlQuery.accept(new DefaultQueryVisitor() { 594 595 @Override 596 public void visitOrderByExpr(OrderByExpr node) { 597 String name = getFieldName(node.reference.name, null); 598 if (NXQL.ECM_FULLTEXT_SCORE.equals(name)) { 599 name = ES_SCORE_FIELD; 600 } 601 sortInfos.add(new SortInfo(name, !node.isDescending)); 602 } 603 }); 604 return sortInfos; 605 } 606 607 public static Map<String, Type> getSelectClauseFields(String nxql) { 608 final Map<String, Type> fieldsAndTypes = new LinkedHashMap<>(); 609 SQLQuery nxqlQuery = getSqlQuery(nxql); 610 nxqlQuery.accept(new DefaultQueryVisitor() { 611 612 @Override 613 public void visitSelectClause(SelectClause selectClause) { 614 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 615 for (int i = 0; i < selectClause.getSelectList().size(); i++) { 616 Operand op = selectClause.get(i); 617 if (!(op instanceof Reference)) { 618 // ignore it 619 continue; 620 } 621 String name = ((Reference) op).name; 622 Field field = schemaManager.getField(name); 623 fieldsAndTypes.put(name, field == null ? null : field.getType()); 624 } 625 } 626 }); 627 return fieldsAndTypes; 628 } 629 630 /** 631 * Translates from Nuxeo syntax to Elasticsearch simple_query_string syntax. 632 */ 633 public static String translateFulltextQuery(String query) { 634 // The AND operator does not exist in NXQL it is the default operator 635 return query.replace(" OR ", " | ").replace(" or ", " | "); 636 } 637 638 /** 639 * Class to hold both a query and a filter 640 */ 641 public static class QueryAndFilter { 642 643 public final QueryBuilder query; 644 645 public final QueryBuilder filter; 646 647 public QueryAndFilter(QueryBuilder query, QueryBuilder filter) { 648 this.query = query; 649 this.filter = filter; 650 } 651 } 652 653 public static class ExpressionBuilder { 654 655 public final String operator; 656 657 public QueryBuilder query; 658 659 public ExpressionBuilder(final String op) { 660 this.operator = op; 661 this.query = null; 662 } 663 664 public void add(final QueryAndFilter qf) { 665 if (qf != null) { 666 add(qf.query, qf.filter); 667 } 668 } 669 670 public void add(QueryBuilder q) { 671 add(q, null); 672 } 673 674 public void add(final QueryBuilder q, final QueryBuilder f) { 675 if (q == null && f == null) { 676 return; 677 } 678 QueryBuilder inputQuery = q; 679 if (inputQuery == null) { 680 inputQuery = QueryBuilders.constantScoreQuery(f); 681 } 682 if (operator == null) { 683 // first level expression 684 query = inputQuery; 685 } else { 686 // boolean query 687 if (query == null) { 688 query = QueryBuilders.boolQuery(); 689 } 690 BoolQueryBuilder boolQuery = (BoolQueryBuilder) query; 691 if ("AND".equals(operator)) { 692 boolQuery.must(inputQuery); 693 } else if ("OR".equals(operator)) { 694 boolQuery.should(inputQuery); 695 } else if ("NOT".equals(operator)) { 696 boolQuery.mustNot(inputQuery); 697 } 698 } 699 } 700 701 public void merge(ExpressionBuilder expr) { 702 if ((expr.operator != null) && expr.operator.equals(operator) && (query == null)) { 703 query = expr.query; 704 } else { 705 add(new QueryAndFilter(expr.query, null)); 706 } 707 } 708 709 public QueryBuilder get() { 710 if (query == null) { 711 return QueryBuilders.matchAllQuery(); 712 } 713 return query; 714 } 715 716 @Override 717 public String toString() { 718 return query.toString(); 719 } 720 721 } 722}