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