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.ES_SCORE_FIELD;
023import static org.nuxeo.elasticsearch.ElasticSearchConstants.FULLTEXT_FIELD;
024
025import java.io.StringReader;
026import java.time.ZonedDateTime;
027import java.util.ArrayList;
028import java.util.Calendar;
029import java.util.Collection;
030import java.util.GregorianCalendar;
031import java.util.Iterator;
032import java.util.LinkedHashMap;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038import org.apache.commons.lang3.ArrayUtils;
039import org.apache.commons.lang3.StringUtils;
040import org.elasticsearch.index.query.BoolQueryBuilder;
041import org.elasticsearch.index.query.MatchPhrasePrefixQueryBuilder;
042import org.elasticsearch.index.query.MoreLikeThisQueryBuilder;
043import org.elasticsearch.index.query.QueryBuilder;
044import org.elasticsearch.index.query.QueryBuilders;
045import org.elasticsearch.index.query.SimpleQueryStringBuilder;
046import org.nuxeo.ecm.core.api.CoreSession;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.DocumentNotFoundException;
049import org.nuxeo.ecm.core.api.IdRef;
050import org.nuxeo.ecm.core.api.LifeCycleConstants;
051import org.nuxeo.ecm.core.api.SortInfo;
052import org.nuxeo.ecm.core.api.trash.TrashService;
053import org.nuxeo.ecm.core.api.trash.TrashService.Feature;
054import org.nuxeo.ecm.core.query.QueryParseException;
055import org.nuxeo.ecm.core.query.sql.NXQL;
056import org.nuxeo.ecm.core.query.sql.SQLQueryParser;
057import org.nuxeo.ecm.core.query.sql.model.DefaultQueryVisitor;
058import org.nuxeo.ecm.core.query.sql.model.EsHint;
059import org.nuxeo.ecm.core.query.sql.model.Expression;
060import org.nuxeo.ecm.core.query.sql.model.FromClause;
061import org.nuxeo.ecm.core.query.sql.model.FromList;
062import org.nuxeo.ecm.core.query.sql.model.Function;
063import org.nuxeo.ecm.core.query.sql.model.Literal;
064import org.nuxeo.ecm.core.query.sql.model.LiteralList;
065import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
066import org.nuxeo.ecm.core.query.sql.model.Operand;
067import org.nuxeo.ecm.core.query.sql.model.Operator;
068import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
069import org.nuxeo.ecm.core.query.sql.model.Predicate;
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.query.sql.model.StringLiteral;
074import org.nuxeo.ecm.core.schema.SchemaManager;
075import org.nuxeo.ecm.core.schema.types.Field;
076import org.nuxeo.ecm.core.schema.types.Type;
077import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
078import org.nuxeo.ecm.core.schema.utils.DateParser;
079import org.nuxeo.ecm.core.security.SecurityService;
080import org.nuxeo.ecm.core.storage.sql.jdbc.NXQLQueryMaker;
081import org.nuxeo.elasticsearch.api.ElasticSearchAdmin;
082import org.nuxeo.elasticsearch.hint.MoreLikeThisESHintQueryBuilder;
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
091    protected static final String SELECT_ALL = "SELECT * FROM Document";
092
093    protected static final String SELECT_ALL_WHERE = "SELECT * FROM Document WHERE ";
094
095    protected static final String SIMPLE_QUERY_PREFIX = "es: ";
096
097    /**
098     * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#MORE_LIKE_THIS_MIN_TERM_FREQ} instead.
099     */
100    @Deprecated
101    protected static final int MORE_LIKE_THIS_MIN_TERM_FREQ = MoreLikeThisESHintQueryBuilder.MORE_LIKE_THIS_MIN_TERM_FREQ;
102
103    /**
104     * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#MORE_LIKE_THIS_MIN_DOC_FREQ} instead.
105     */
106    @Deprecated
107    protected static final int MORE_LIKE_THIS_MIN_DOC_FREQ = MoreLikeThisESHintQueryBuilder.MORE_LIKE_THIS_MIN_DOC_FREQ;
108
109    /**
110     * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#MORE_LIKE_THIS_MAX_QUERY_TERMS} instead.
111     */
112    @Deprecated
113    protected static final int MORE_LIKE_THIS_MAX_QUERY_TERMS = MoreLikeThisESHintQueryBuilder.MORE_LIKE_THIS_MAX_QUERY_TERMS;
114
115    private NxqlQueryConverter() {
116    }
117
118    public static QueryBuilder toESQueryBuilder(final String nxql) {
119        return toESQueryBuilder(nxql, null);
120    }
121
122    public static QueryBuilder toESQueryBuilder(final String nxql, final CoreSession session) {
123        final LinkedList<ExpressionBuilder> builders = new LinkedList<>();
124        SQLQuery nxqlQuery = getSqlQuery(nxql);
125        if (session != null) {
126            nxqlQuery = addSecurityPolicy(session, nxqlQuery);
127        }
128        final ExpressionBuilder ret = new ExpressionBuilder(null);
129        builders.add(ret);
130        final ArrayList<String> fromList = new ArrayList<>();
131        nxqlQuery.accept(new DefaultQueryVisitor() {
132
133            @Override
134            public void visitFromClause(FromClause node) {
135                FromList elements = node.elements;
136                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
137
138                for (String type : elements.values()) {
139                    if (NXQLQueryMaker.TYPE_DOCUMENT.equalsIgnoreCase(type)) {
140                        // From Document means all doc types
141                        fromList.clear();
142                        return;
143                    }
144                    Set<String> types = schemaManager.getDocumentTypeNamesExtending(type);
145                    if (types != null) {
146                        fromList.addAll(types);
147                    }
148                }
149            }
150
151            @Override
152            public void visitMultiExpression(MultiExpression node) {
153                for (Iterator<Predicate> it = node.predicates.iterator(); it.hasNext();) {
154                    it.next().accept(this);
155                    if (it.hasNext()) {
156                        node.operator.accept(this);
157                    }
158                }
159            }
160
161            @Override
162            public void visitSelectClause(SelectClause node) {
163                // NOP
164            }
165
166            @Override
167            public void visitExpression(Expression node) {
168                Operator op = node.operator;
169                if (op == Operator.AND || op == Operator.OR || op == Operator.NOT) {
170                    builders.add(new ExpressionBuilder(op.toString()));
171                    super.visitExpression(node);
172                    ExpressionBuilder expr = builders.removeLast();
173                    if (!builders.isEmpty()) {
174                        builders.getLast().merge(expr);
175                    }
176                } else {
177                    Reference ref = node.lvalue instanceof Reference ? (Reference) node.lvalue : null;
178                    String name = ref != null ? ref.name : node.lvalue.toString();
179                    String value = null;
180                    if (node.rvalue instanceof Literal) {
181                        value = ((Literal) node.rvalue).asString();
182                    } else if (node.rvalue instanceof Function) {
183                        Function function = (Function) node.rvalue;
184                        String func = function.name;
185                        if (NXQL.NOW_FUNCTION.equalsIgnoreCase(func)) {
186                            String periodAndDurationText;
187                            if (function.args == null || function.args.size() != 1) {
188                                periodAndDurationText = null;
189                            } else {
190                                periodAndDurationText = ((StringLiteral) function.args.get(0)).value;
191                            }
192                            ZonedDateTime dateTime = NXQL.nowPlusPeriodAndDuration(periodAndDurationText);
193                            Calendar calendar = GregorianCalendar.from(dateTime);
194                            value = DateParser.formatW3CDateTime(calendar);
195                        } else {
196                            throw new IllegalArgumentException("Unknown function: " + func);
197                        }
198                    } else if (node.rvalue != null) {
199                        value = node.rvalue.toString();
200                    }
201                    Object[] values = null;
202                    if (node.rvalue instanceof LiteralList) {
203                        LiteralList items = (LiteralList) node.rvalue;
204                        values = new Object[items.size()];
205                        int i = 0;
206                        for (Literal item : items) {
207                            values[i++] = item.asString();
208                        }
209                    }
210                    // add expression to the last builder
211                    EsHint hint = (ref != null) ? ref.esHint : null;
212                    builders.getLast()
213                            .add(makeQueryFromSimpleExpression(op.toString(), name, value, values, hint, session));
214                }
215            }
216        });
217        QueryBuilder queryBuilder = ret.get();
218        if (!fromList.isEmpty()) {
219            return QueryBuilders.boolQuery()
220                                .must(queryBuilder)
221                                .filter(makeQueryFromSimpleExpression("IN", NXQL.ECM_PRIMARYTYPE, null,
222                                        fromList.toArray(), null, null).filter);
223        }
224        return queryBuilder;
225    }
226
227    protected static SQLQuery getSqlQuery(String nxql) {
228        String query = completeQueryWithSelect(nxql);
229        SQLQuery nxqlQuery;
230        try {
231            nxqlQuery = SQLQueryParser.parse(new StringReader(query));
232        } catch (QueryParseException e) {
233            e.addInfo("Query: " + query);
234            throw e;
235        }
236        return nxqlQuery;
237    }
238
239    protected static SQLQuery addSecurityPolicy(CoreSession session, SQLQuery query) {
240        Collection<SQLQuery.Transformer> transformers = Framework.getService(SecurityService.class)
241                                                                 .getPoliciesQueryTransformers(
242                                                                         session.getRepositoryName());
243        for (SQLQuery.Transformer trans : transformers) {
244            query = trans.transform(session.getPrincipal(), query);
245        }
246        return query;
247    }
248
249    protected static String completeQueryWithSelect(String nxql) {
250        String query = (nxql == null) ? "" : nxql.trim();
251        if (query.isEmpty()) {
252            query = SELECT_ALL;
253        } else if (!query.toLowerCase().startsWith("select ")) {
254            query = SELECT_ALL_WHERE + nxql;
255        }
256        return query;
257    }
258
259    public static QueryAndFilter makeQueryFromSimpleExpression(String op, String nxqlName, Object value,
260            Object[] values, EsHint hint, CoreSession session) {
261        QueryBuilder query = null;
262        QueryBuilder filter = null;
263        String name = getFieldName(nxqlName, hint);
264        if (hint != null && hint.operator != null) {
265            if (ArrayUtils.isNotEmpty(values)) {
266                filter = makeHintQuery(name, values, hint);
267            } else {
268                query = makeHintQuery(name, value, hint);
269            }
270        } else if (nxqlName.startsWith(NXQL.ECM_FULLTEXT) && ("=".equals(op) || "!=".equals(op) || "<>".equals(op)
271                || "LIKE".equals(op) || "NOT LIKE".equals(op))) {
272            query = makeFulltextQuery(nxqlName, (String) value, hint);
273            if ("!=".equals(op) || "<>".equals(op) || "NOT LIKE".equals(op)) {
274                filter = QueryBuilders.boolQuery().mustNot(query);
275                query = null;
276            }
277        } else if (nxqlName.startsWith(NXQL.ECM_ANCESTORID)) {
278            filter = makeAncestorIdFilter((String) value, session);
279            if ("!=".equals(op) || "<>".equals(op)) {
280                filter = QueryBuilders.boolQuery().mustNot(filter);
281            }
282        } else if (nxqlName.equals(NXQL.ECM_ISTRASHED)) {
283            filter = makeTrashedFilter(op, name, (String) value);
284
285        } else
286            switch (op) {
287            case "=":
288                filter = QueryBuilders.termQuery(name, checkBoolValue(nxqlName, value));
289                break;
290            case "<>":
291            case "!=":
292                filter = QueryBuilders.boolQuery()
293                                      .mustNot(QueryBuilders.termQuery(name, checkBoolValue(nxqlName, value)));
294                break;
295            case ">":
296                filter = QueryBuilders.rangeQuery(name).gt(value);
297                break;
298            case "<":
299                filter = QueryBuilders.rangeQuery(name).lt(value);
300                break;
301            case ">=":
302                filter = QueryBuilders.rangeQuery(name).gte(value);
303                break;
304            case "<=":
305                filter = QueryBuilders.rangeQuery(name).lte(value);
306                break;
307            case "BETWEEN":
308            case "NOT BETWEEN":
309                filter = QueryBuilders.rangeQuery(name).from(values[0]).to(values[1]);
310                if (op.startsWith("NOT")) {
311                    filter = QueryBuilders.boolQuery().mustNot(filter);
312                }
313                break;
314            case "IN":
315            case "NOT IN":
316                filter = QueryBuilders.termsQuery(name, values);
317                if (op.startsWith("NOT")) {
318                    filter = QueryBuilders.boolQuery().mustNot(filter);
319                }
320                break;
321            case "IS NULL":
322                filter = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(name));
323                break;
324            case "IS NOT NULL":
325                filter = QueryBuilders.existsQuery(name);
326                break;
327            case "LIKE":
328            case "ILIKE":
329            case "NOT LIKE":
330            case "NOT ILIKE":
331                query = makeLikeQuery(op, name, (String) value, hint);
332                if (op.startsWith("NOT")) {
333                    filter = QueryBuilders.boolQuery().mustNot(query);
334                    query = null;
335                }
336                break;
337            case "STARTSWITH":
338                filter = makeStartsWithQuery(name, value);
339                break;
340            default:
341                throw new UnsupportedOperationException("Operator: '" + op + "' is unknown");
342            }
343        return new QueryAndFilter(query, filter);
344    }
345
346    protected static Object checkBoolValue(String nxqlName, Object value) {
347        if (!"0".equals(value) && !"1".equals(value)) {
348            return value;
349        }
350        switch (nxqlName) {
351        case NXQL.ECM_ISPROXY:
352        case NXQL.ECM_ISCHECKEDIN:
353        case NXQL.ECM_ISTRASHED:
354        case NXQL.ECM_ISVERSION:
355        case NXQL.ECM_ISVERSION_OLD:
356        case NXQL.ECM_ISRECORD:
357        case NXQL.ECM_HASLEGALHOLD:
358        case NXQL.ECM_ISLATESTMAJORVERSION:
359        case NXQL.ECM_ISLATESTVERSION:
360            break;
361        default:
362            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
363            Field field = schemaManager.getField(nxqlName);
364            if (field == null || !BooleanType.ID.equals(field.getType().getName())) {
365                return value;
366            }
367        }
368        return "0".equals(value) ? "false" : "true";
369    }
370
371    protected static QueryBuilder makeTrashedFilter(String op, String name, String value) {
372        boolean equalsDeleted;
373        switch (op) {
374        case "=":
375            equalsDeleted = true;
376            break;
377        case "<>":
378        case "!=":
379            equalsDeleted = false;
380            break;
381        default:
382            throw new IllegalArgumentException(NXQL.ECM_ISTRASHED + " requires = or <> operator");
383        }
384        if ("0".equals(value)) {
385            equalsDeleted = !equalsDeleted;
386        } else if ("1".equals(value)) {
387            // equalsDeleted unchanged
388        } else {
389            throw new IllegalArgumentException(NXQL.ECM_ISTRASHED + " requires literal 0 or 1 as right argument");
390        }
391        TrashService trashService = Framework.getService(TrashService.class);
392        QueryBuilder filter = null;
393        if (trashService.hasFeature(Feature.TRASHED_STATE_IS_DEDUCED_FROM_LIFECYCLE)) {
394            filter = QueryBuilders.termQuery(NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE);
395        } else if (trashService.hasFeature(Feature.TRASHED_STATE_IN_MIGRATION)) {
396            filter = QueryBuilders.boolQuery()
397                                  .should(QueryBuilders.termQuery(NXQL.ECM_LIFECYCLESTATE,
398                                          LifeCycleConstants.DELETED_STATE))
399                                  .should(QueryBuilders.termQuery(name, true));
400        } else if (trashService.hasFeature(Feature.TRASHED_STATE_IS_DEDICATED_PROPERTY)) {
401            filter = QueryBuilders.termQuery(name, true);
402        }
403        if (!equalsDeleted) {
404            filter = QueryBuilders.boolQuery().mustNot(filter);
405        }
406        return filter;
407    }
408
409    protected static QueryBuilder makeHintQuery(String name, Object value, EsHint hint) {
410        return Framework.getService(ElasticSearchAdmin.class)
411                        .getHintByOperator(hint.operator)
412                        .orElseThrow(() -> new UnsupportedOperationException(
413                                String.format("Operator: %s is unknown", hint.operator)))
414                        .make(hint, name, value);
415    }
416
417    /**
418     * @deprecated since 11.1. Use {@link MoreLikeThisESHintQueryBuilder#getItems(Object)} instead.
419     */
420    @Deprecated
421    protected static MoreLikeThisQueryBuilder.Item[] getItems(Object value) {
422        return MoreLikeThisESHintQueryBuilder.getItems(value);
423    }
424
425    public static QueryBuilder makeStartsWithQuery(String name, Object value) {
426        QueryBuilder filter;
427        String indexName = name + ".children";
428        if ("/".equals(value)) {
429            if (NXQL.ECM_PATH.equals(name)) {
430                // any non orphan|place-less document must have a path starting with "/"
431                filter = QueryBuilders.existsQuery(NXQL.ECM_PARENTID);
432            } else {
433                // match any document with a populated field
434                filter = QueryBuilders.existsQuery(indexName);
435            }
436        } else {
437            String v = String.valueOf(value);
438            if (v.endsWith("/")) {
439                v = v.replaceAll("/$", "");
440            }
441            if (NXQL.ECM_PATH.equals(name)) {
442                // we don't want to return the parent when searching on ecm:path, see NXP-18955
443                filter = QueryBuilders.boolQuery()
444                                      .must(QueryBuilders.termQuery(indexName, v))
445                                      .mustNot(QueryBuilders.termQuery(name, value));
446            } else {
447                filter = QueryBuilders.termQuery(indexName, v);
448            }
449        }
450        return filter;
451    }
452
453    protected static QueryBuilder makeAncestorIdFilter(String value, CoreSession session) {
454        String path;
455        if (session == null) {
456            return QueryBuilders.existsQuery("ancestorid-without-session");
457        } else {
458            try {
459                DocumentModel doc = session.getDocument(new IdRef(value));
460                path = doc.getPathAsString();
461            } catch (DocumentNotFoundException e) {
462                return QueryBuilders.existsQuery("ancestorid-not-found");
463            }
464        }
465        return makeStartsWithQuery(NXQL.ECM_PATH, path);
466    }
467
468    protected static QueryBuilder makeLikeQuery(String op, String name, String value, EsHint hint) {
469        String fieldName = name;
470        if (op.contains("ILIKE")) {
471            // ILIKE will work only with a correct mapping
472            value = value.toLowerCase();
473            fieldName = name + ".lowercase";
474        }
475        if (hint != null && hint.index != null) {
476            fieldName = hint.index;
477        }
478        // convert the value to a wildcard query
479        String wildcard = likeToWildcard(value);
480        // use match phrase prefix when possible
481        if (StringUtils.countMatches(wildcard, "*") == 1 && wildcard.endsWith("*") && !wildcard.contains("?")
482                && !wildcard.contains("\\")) {
483            MatchPhrasePrefixQueryBuilder query = QueryBuilders.matchPhrasePrefixQuery(fieldName,
484                    wildcard.replace("*", ""));
485            if (hint != null && hint.analyzer != null) {
486                query.analyzer(hint.analyzer);
487            }
488            return query;
489        }
490        return QueryBuilders.wildcardQuery(fieldName, wildcard);
491    }
492
493    /**
494     * Turns a NXQL LIKE pattern into a wildcard for WildcardQuery.
495     * <p>
496     * % and _ are standard wildcards, and \ escapes them.
497     *
498     * @since 7.4
499     */
500    protected static String likeToWildcard(String like) {
501        StringBuilder wildcard = new StringBuilder();
502        char[] chars = like.toCharArray();
503        boolean escape = false;
504        for (char c : chars) {
505            boolean escapeNext = false;
506            switch (c) {
507            case '?':
508                wildcard.append("\\?");
509                break;
510            case '*': // compat, * = % in NXQL (for some backends)
511            case '%':
512                if (escape) {
513                    wildcard.append(c);
514                } else {
515                    wildcard.append("*");
516                }
517                break;
518            case '_':
519                if (escape) {
520                    wildcard.append(c);
521                } else {
522                    wildcard.append("?");
523                }
524                break;
525            case '\\':
526                if (escape) {
527                    wildcard.append("\\\\");
528                } else {
529                    escapeNext = true;
530                }
531                break;
532            default:
533                wildcard.append(c);
534                break;
535            }
536            escape = escapeNext;
537        }
538        if (escape) {
539            // invalid string terminated by escape character, ignore
540        }
541        return wildcard.toString();
542    }
543
544    protected static QueryBuilder makeFulltextQuery(String nxqlName, String value, EsHint hint) {
545        String name = nxqlName.replace(NXQL.ECM_FULLTEXT, "");
546        if (name.startsWith(".")) {
547            name = name.substring(1) + ".fulltext";
548        } else {
549            // map ecm:fulltext_someindex to default
550            name = FULLTEXT_FIELD;
551        }
552        String queryString = value;
553        org.elasticsearch.index.query.Operator defaultOperator;
554        if (queryString.startsWith(SIMPLE_QUERY_PREFIX)) {
555            // elasticsearch-specific syntax
556            queryString = queryString.substring(SIMPLE_QUERY_PREFIX.length());
557            defaultOperator = org.elasticsearch.index.query.Operator.OR;
558        } else {
559            queryString = translateFulltextQuery(queryString);
560            defaultOperator = org.elasticsearch.index.query.Operator.AND;
561        }
562        String analyzer = (hint != null && hint.analyzer != null) ? hint.analyzer : "fulltext";
563        SimpleQueryStringBuilder query = QueryBuilders.simpleQueryStringQuery(queryString)
564                                                      .defaultOperator(defaultOperator)
565                                                      .analyzer(analyzer);
566        if (hint != null && hint.index != null) {
567            for (EsHint.FieldHint fieldHint : hint.getIndex()) {
568                query.field(fieldHint.getField(), fieldHint.getBoost());
569            }
570        } else {
571            query.field(name);
572        }
573        return query;
574    }
575
576    protected static String getFieldName(String name, EsHint hint) {
577        if (hint != null && hint.index != null) {
578            return hint.index;
579        }
580        // compat
581        if (NXQL.ECM_ISVERSION_OLD.equals(name)) {
582            name = NXQL.ECM_ISVERSION;
583        }
584        // complex field
585        name = name.replace("/*", "");
586        name = name.replace("/", ".");
587        return name;
588    }
589
590    public static List<SortInfo> getSortInfo(String nxql) {
591        final List<SortInfo> sortInfos = new ArrayList<>();
592        SQLQuery nxqlQuery = getSqlQuery(nxql);
593        nxqlQuery.accept(new DefaultQueryVisitor() {
594
595            @Override
596            public void visitOrderByExpr(OrderByExpr node) {
597                String name = getFieldName(node.reference.name, null);
598                if (NXQL.ECM_FULLTEXT_SCORE.equals(name)) {
599                    name = ES_SCORE_FIELD;
600                }
601                sortInfos.add(new SortInfo(name, !node.isDescending));
602            }
603        });
604        return sortInfos;
605    }
606
607    public static Map<String, Type> getSelectClauseFields(String nxql) {
608        final Map<String, Type> fieldsAndTypes = new LinkedHashMap<>();
609        SQLQuery nxqlQuery = getSqlQuery(nxql);
610        nxqlQuery.accept(new DefaultQueryVisitor() {
611
612            @Override
613            public void visitSelectClause(SelectClause selectClause) {
614                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
615                for (int i = 0; i < selectClause.getSelectList().size(); i++) {
616                    Operand op = selectClause.get(i);
617                    if (!(op instanceof Reference)) {
618                        // ignore it
619                        continue;
620                    }
621                    String name = ((Reference) op).name;
622                    Field field = schemaManager.getField(name);
623                    fieldsAndTypes.put(name, field == null ? null : field.getType());
624                }
625            }
626        });
627        return fieldsAndTypes;
628    }
629
630    /**
631     * Translates from Nuxeo syntax to Elasticsearch simple_query_string syntax.
632     */
633    public static String translateFulltextQuery(String query) {
634        // The AND operator does not exist in NXQL it is the default operator
635        return query.replace(" OR ", " | ").replace(" or ", " | ");
636    }
637
638    /**
639     * Class to hold both a query and a filter
640     */
641    public static class QueryAndFilter {
642
643        public final QueryBuilder query;
644
645        public final QueryBuilder filter;
646
647        public QueryAndFilter(QueryBuilder query, QueryBuilder filter) {
648            this.query = query;
649            this.filter = filter;
650        }
651    }
652
653    public static class ExpressionBuilder {
654
655        public final String operator;
656
657        public QueryBuilder query;
658
659        public ExpressionBuilder(final String op) {
660            this.operator = op;
661            this.query = null;
662        }
663
664        public void add(final QueryAndFilter qf) {
665            if (qf != null) {
666                add(qf.query, qf.filter);
667            }
668        }
669
670        public void add(QueryBuilder q) {
671            add(q, null);
672        }
673
674        public void add(final QueryBuilder q, final QueryBuilder f) {
675            if (q == null && f == null) {
676                return;
677            }
678            QueryBuilder inputQuery = q;
679            if (inputQuery == null) {
680                inputQuery = QueryBuilders.constantScoreQuery(f);
681            }
682            if (operator == null) {
683                // first level expression
684                query = inputQuery;
685            } else {
686                // boolean query
687                if (query == null) {
688                    query = QueryBuilders.boolQuery();
689                }
690                BoolQueryBuilder boolQuery = (BoolQueryBuilder) query;
691                if ("AND".equals(operator)) {
692                    boolQuery.must(inputQuery);
693                } else if ("OR".equals(operator)) {
694                    boolQuery.should(inputQuery);
695                } else if ("NOT".equals(operator)) {
696                    boolQuery.mustNot(inputQuery);
697                }
698            }
699        }
700
701        public void merge(ExpressionBuilder expr) {
702            if ((expr.operator != null) && expr.operator.equals(operator) && (query == null)) {
703                query = expr.query;
704            } else {
705                add(new QueryAndFilter(expr.query, null));
706            }
707        }
708
709        public QueryBuilder get() {
710            if (query == null) {
711                return QueryBuilders.matchAllQuery();
712            }
713            return query;
714        }
715
716        @Override
717        public String toString() {
718            return query.toString();
719        }
720
721    }
722}