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