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