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