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}