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