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