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