001/*
002 * (C) Copyright 2006-2017 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 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.opencmis.impl.server;
020
021import static org.apache.chemistry.opencmis.commons.enums.BaseTypeId.CMIS_DOCUMENT;
022import static org.apache.chemistry.opencmis.commons.enums.BaseTypeId.CMIS_RELATIONSHIP;
023
024import java.io.Serializable;
025import java.math.BigDecimal;
026import java.math.BigInteger;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Calendar;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.LinkedList;
036import java.util.List;
037import java.util.Map;
038import java.util.Map.Entry;
039import java.util.Set;
040import java.util.stream.Collectors;
041
042import org.antlr.runtime.RecognitionException;
043import org.antlr.runtime.tree.Tree;
044import org.apache.chemistry.opencmis.commons.PropertyIds;
045import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
046import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
047import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
048import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
049import org.apache.chemistry.opencmis.commons.enums.Cardinality;
050import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
051import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
052import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalDefinitionImpl;
053import org.apache.chemistry.opencmis.server.support.query.AbstractPredicateWalker;
054import org.apache.chemistry.opencmis.server.support.query.CmisSelector;
055import org.apache.chemistry.opencmis.server.support.query.ColumnReference;
056import org.apache.chemistry.opencmis.server.support.query.FunctionReference;
057import org.apache.chemistry.opencmis.server.support.query.FunctionReference.CmisQlFunction;
058import org.apache.chemistry.opencmis.server.support.query.QueryObject;
059import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
060import org.apache.chemistry.opencmis.server.support.query.QueryUtilStrict;
061import org.apache.commons.lang.StringUtils;
062import org.apache.commons.logging.Log;
063import org.apache.commons.logging.LogFactory;
064import org.joda.time.LocalDateTime;
065import org.joda.time.format.DateTimeFormatter;
066import org.joda.time.format.ISODateTimeFormat;
067import org.nuxeo.ecm.core.api.CoreSession;
068import org.nuxeo.ecm.core.api.DocumentRef;
069import org.nuxeo.ecm.core.api.IdRef;
070import org.nuxeo.ecm.core.api.IterableQueryResult;
071import org.nuxeo.ecm.core.api.LifeCycleConstants;
072import org.nuxeo.ecm.core.api.PartialList;
073import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
074import org.nuxeo.ecm.core.query.QueryParseException;
075import org.nuxeo.ecm.core.query.sql.NXQL;
076import org.nuxeo.ecm.core.schema.SchemaManager;
077import org.nuxeo.ecm.core.schema.types.Schema;
078import org.nuxeo.runtime.api.Framework;
079import org.nuxeo.runtime.services.config.ConfigurationService;
080
081/**
082 * Transformer of CMISQL queries into NXQL queries.
083 */
084public class CMISQLtoNXQL {
085
086    private static final Log log = LogFactory.getLog(CMISQLtoNXQL.class);
087
088    protected static final String CMIS_PREFIX = "cmis:";
089
090    protected static final String NX_PREFIX = "nuxeo:";
091
092    protected static final String NXQL_DOCUMENT = "Document";
093
094    protected static final String NXQL_RELATION = "Relation";
095
096    protected static final String NXQL_DC_TITLE = "dc:title";
097
098    protected static final String NXQL_DC_DESCRIPTION = "dc:description";
099
100    protected static final String NXQL_DC_CREATOR = "dc:creator";
101
102    protected static final String NXQL_DC_CREATED = "dc:created";
103
104    protected static final String NXQL_DC_MODIFIED = "dc:modified";
105
106    protected static final String NXQL_DC_LAST_CONTRIBUTOR = "dc:lastContributor";
107
108    protected static final String NXQL_REL_SOURCE = "relation:source";
109
110    protected static final String NXQL_REL_TARGET = "relation:target";
111
112    protected static final DateTimeFormatter ISO_DATE_TIME_FORMAT = ISODateTimeFormat.dateTime();
113
114    private static final char QUOTE = '\'';
115
116    private static final String SPACE_ASC = " asc";
117
118    private static final String SPACE_DESC = " desc";
119
120    // list of SQL column where NULL (missing value) should be treated as
121    // Boolean.FALSE in a query result
122    protected static final Set<String> NULL_IS_FALSE_COLUMNS = new HashSet<>(Arrays.asList(NXQL.ECM_ISVERSION,
123            NXQL.ECM_ISLATESTVERSION, NXQL.ECM_ISLATESTMAJORVERSION, NXQL.ECM_ISCHECKEDIN));
124
125    protected final boolean supportsProxies;
126
127    protected Map<String, PropertyDefinition<?>> typeInfo;
128
129    protected CoreSession coreSession;
130
131    // ----- filled during walks of the clauses -----
132
133    protected QueryUtilStrict queryUtil;
134
135    protected QueryObject query;
136
137    protected TypeDefinition fromType;
138
139    protected boolean skipDeleted = true;
140
141    // ----- passed to IterableQueryResult -----
142
143    /** The real columns, CMIS name mapped to NXQL. */
144    protected Map<String, String> realColumns = new LinkedHashMap<>();
145
146    /** The non-real-columns we'll return as well. */
147    protected Map<String, ColumnReference> virtualColumns = new LinkedHashMap<>();
148
149    public CMISQLtoNXQL(boolean supportsProxies) {
150        this.supportsProxies = supportsProxies;
151    }
152
153    /**
154     * Gets the NXQL from a CMISQL query.
155     */
156    public String getNXQL(String cmisql, NuxeoCmisService service, Map<String, PropertyDefinition<?>> typeInfo,
157            boolean searchAllVersions) throws QueryParseException {
158        this.typeInfo = typeInfo;
159        boolean searchLatestVersion = !searchAllVersions;
160        TypeManagerImpl typeManager = service.getTypeManager();
161        coreSession = service.coreSession;
162
163        try {
164            queryUtil = new QueryUtilStrict(cmisql, typeManager, new AnalyzingWalker(), false);
165            queryUtil.processStatement();
166            query = queryUtil.getQueryObject();
167        } catch (RecognitionException e) {
168            throw new QueryParseException(queryUtil.getErrorMessage(e), e);
169        }
170        if (query.getTypes().size() != 1 && query.getJoinedSecondaryTypes() == null) {
171            throw new QueryParseException("JOINs not supported in query: " + cmisql);
172        }
173
174        fromType = query.getMainFromName();
175        BaseTypeId fromBaseTypeId = fromType.getBaseTypeId();
176
177        // now resolve column selectors to actual database columns
178        for (CmisSelector sel : query.getSelectReferences()) {
179            recordSelectSelector(sel);
180        }
181        for (CmisSelector sel : query.getJoinReferences()) {
182            ColumnReference col = ((ColumnReference) sel);
183            if (col.getTypeDefinition().getBaseTypeId() == BaseTypeId.CMIS_SECONDARY) {
184                // ignore reference to ON FACET.cmis:objectId
185                continue;
186            }
187            recordSelector(sel, JOIN);
188        }
189        for (CmisSelector sel : query.getWhereReferences()) {
190            recordSelector(sel, WHERE);
191        }
192        for (SortSpec spec : query.getOrderBys()) {
193            recordSelector(spec.getSelector(), ORDER_BY);
194        }
195
196        addSystemColumns();
197
198        List<String> whereClauses = new ArrayList<>();
199
200        // what to select (result columns)
201
202        String what = StringUtils.join(realColumns.values(), ", ");
203
204        // determine relevant primary types
205
206        String nxqlFrom;
207        if (fromBaseTypeId == CMIS_RELATIONSHIP) {
208            if (fromType.getParentTypeId() == null) {
209                nxqlFrom = NXQL_RELATION;
210            } else {
211                nxqlFrom = fromType.getId();
212            }
213        } else {
214            nxqlFrom = NXQL_DOCUMENT;
215            List<String> types = new ArrayList<>();
216            if (fromType.getParentTypeId() != null) {
217                // don't add abstract root types
218                types.add(fromType.getId());
219            }
220            LinkedList<TypeDefinitionContainer> typesTodo = new LinkedList<>();
221            typesTodo.addAll(typeManager.getTypeDescendants(fromType.getId(), -1, Boolean.TRUE));
222            // recurse to get all subtypes
223            TypeDefinitionContainer tc;
224            while ((tc = typesTodo.poll()) != null) {
225                types.add(tc.getTypeDefinition().getId());
226                typesTodo.addAll(tc.getChildren());
227            }
228            if (types.isEmpty()) {
229                // shoudn't happen
230                types = Collections.singletonList("__NOSUCHTYPE__");
231            }
232            // build clause
233            StringBuilder pt = new StringBuilder();
234            pt.append(NXQL.ECM_PRIMARYTYPE);
235            pt.append(" IN (");
236            for (Iterator<String> it = types.iterator(); it.hasNext();) {
237                pt.append(QUOTE);
238                pt.append(it.next());
239                pt.append(QUOTE);
240                if (it.hasNext()) {
241                    pt.append(", ");
242                }
243            }
244            pt.append(")");
245            whereClauses.add(pt.toString());
246        }
247
248        // lifecycle not deleted filter
249
250        if (skipDeleted) {
251            whereClauses.add(String.format("%s <> '%s'", NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE));
252        }
253
254        // searchAllVersions filter
255
256        if (searchLatestVersion && fromBaseTypeId == CMIS_DOCUMENT) {
257            whereClauses.add(String.format("%s = 1", NXQL.ECM_ISLATESTVERSION));
258        }
259
260        // no proxies
261
262        if (!supportsProxies) {
263            whereClauses.add(NXQL.ECM_ISPROXY + " = 0");
264        }
265
266        // WHERE clause
267
268        Tree whereNode = queryUtil.getWalker().getWherePredicateTree();
269        if (whereNode != null) {
270            GeneratingWalker generator = new GeneratingWalker();
271            generator.walkPredicate(whereNode);
272            whereClauses.add(generator.buf.toString());
273        }
274
275        // ORDER BY clause
276
277        List<String> orderbys = new ArrayList<>();
278        for (SortSpec spec : query.getOrderBys()) {
279            String orderby;
280            CmisSelector sel = spec.getSelector();
281            if (sel instanceof ColumnReference) {
282                orderby = (String) sel.getInfo();
283            } else {
284                orderby = NXQL.ECM_FULLTEXT_SCORE;
285            }
286            if (!spec.ascending) {
287                orderby += " DESC";
288            }
289            orderbys.add(orderby);
290        }
291
292        // create the whole select
293
294        String where = StringUtils.join(whereClauses, " AND ");
295        String nxql = "SELECT " + what + " FROM " + nxqlFrom + " WHERE " + where;
296        if (!orderbys.isEmpty()) {
297            nxql += " ORDER BY " + StringUtils.join(orderbys, ", ");
298        }
299        // System.err.println("CMIS: " + statement);
300        // System.err.println("NXQL: " + nxql);
301        return nxql;
302    }
303
304    public IterableQueryResult getIterableQueryResult(IterableQueryResult it, NuxeoCmisService service) {
305        return new NXQLtoCMISIterableQueryResult(it, realColumns, virtualColumns, service);
306    }
307
308    public PartialList<Map<String, Serializable>> convertToCMIS(PartialList<Map<String, Serializable>> pl,
309            NuxeoCmisService service) {
310        return pl.stream().map(map -> convertToCMISMap(map, realColumns, virtualColumns, service)).collect(
311                Collectors.collectingAndThen(Collectors.toList(), result -> new PartialList<>(result, pl.totalSize())));
312    }
313
314    protected boolean isFacetsColumn(String name) {
315        return PropertyIds.SECONDARY_OBJECT_TYPE_IDS.equals(name) || NuxeoTypeHelper.NX_FACETS.equals(name);
316    }
317
318    protected void addSystemColumns() {
319        // additional references to cmis:objectId and cmis:objectTypeId
320        for (String propertyId : Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID)) {
321            if (!realColumns.containsKey(propertyId)) {
322                ColumnReference col = new ColumnReference(propertyId);
323                col.setTypeDefinition(propertyId, fromType);
324                recordSelectSelector(col);
325            }
326        }
327    }
328
329    /**
330     * Records a SELECT selector, and associates it to a database column.
331     */
332    protected void recordSelectSelector(CmisSelector sel) {
333        if (sel instanceof FunctionReference) {
334            FunctionReference fr = (FunctionReference) sel;
335            if (fr.getFunction() != CmisQlFunction.SCORE) {
336                throw new CmisRuntimeException("Unknown function: " + fr.getFunction());
337            }
338            String key = fr.getAliasName();
339            if (key == null) {
340                key = "SEARCH_SCORE"; // default, from spec
341            }
342            realColumns.put(key, NXQL.ECM_FULLTEXT_SCORE);
343            if (typeInfo != null) {
344                PropertyDecimalDefinitionImpl pd = new PropertyDecimalDefinitionImpl();
345                pd.setId(key);
346                pd.setQueryName(key);
347                pd.setCardinality(Cardinality.SINGLE);
348                pd.setDisplayName("Score");
349                pd.setLocalName("score");
350                typeInfo.put(key, pd);
351            }
352        } else { // sel instanceof ColumnReference
353            ColumnReference col = (ColumnReference) sel;
354
355            if (col.getPropertyQueryName().equals("*")) {
356                for (PropertyDefinition<?> pd : fromType.getPropertyDefinitions().values()) {
357                    String id = pd.getId();
358                    if ((pd.getCardinality() == Cardinality.SINGLE //
359                            && Boolean.TRUE.equals(pd.isQueryable())) //
360                            || id.equals(PropertyIds.BASE_TYPE_ID)) {
361                        ColumnReference c = new ColumnReference(null, id);
362                        c.setTypeDefinition(id, fromType);
363                        recordSelectSelector(c);
364                    }
365                }
366                return;
367            }
368
369            String key = col.getPropertyQueryName();
370            PropertyDefinition<?> pd = col.getPropertyDefinition();
371            String nxqlCol = getColumn(col);
372            String id = pd.getId();
373            if (nxqlCol != null && pd.getCardinality() == Cardinality.SINGLE && (Boolean.TRUE.equals(pd.isQueryable())
374                    || id.equals(PropertyIds.BASE_TYPE_ID) || id.equals(PropertyIds.OBJECT_TYPE_ID))) {
375                col.setInfo(nxqlCol);
376                realColumns.put(key, nxqlCol);
377            } else {
378                virtualColumns.put(key, col);
379            }
380            if (typeInfo != null) {
381                typeInfo.put(key, pd);
382            }
383        }
384    }
385
386    protected static final String JOIN = "JOIN";
387
388    protected static final String WHERE = "WHERE";
389
390    protected static final String ORDER_BY = "ORDER BY";
391
392    /**
393     * Records a JOIN / WHERE / ORDER BY selector, and associates it to a database column.
394     */
395    protected void recordSelector(CmisSelector sel, String clauseType) {
396        if (sel instanceof FunctionReference) {
397            FunctionReference fr = (FunctionReference) sel;
398            if (clauseType != ORDER_BY) { // == ok
399                throw new QueryParseException("Cannot use function in " + clauseType + " clause: " + fr.getFunction());
400            }
401            // ORDER BY SCORE, nothing further to record
402            return;
403        }
404        ColumnReference col = (ColumnReference) sel;
405
406        // fetch column and associate it to the selector
407        String column = getColumn(col);
408        if (!isFacetsColumn(col.getPropertyId()) && column == null) {
409            throw new QueryParseException(
410                    "Cannot use column in " + clauseType + " clause: " + col.getPropertyQueryName());
411        }
412        col.setInfo(column);
413
414        if (clauseType == WHERE && NuxeoTypeHelper.NX_LIFECYCLE_STATE.equals(col.getPropertyId())) {
415            // explicit lifecycle query: do not include the 'deleted' lifecycle
416            // filter
417            skipDeleted = false;
418        }
419    }
420
421    /**
422     * Finds a NXQL column from a CMIS reference.
423     */
424    protected String getColumn(ColumnReference col) {
425        return getColumn(col.getPropertyId());
426    }
427
428    /**
429     * Finds a NXQL column from a CMIS reference.
430     */
431    protected String getColumn(String propertyId) {
432        if (propertyId.startsWith(CMIS_PREFIX) || propertyId.startsWith(NX_PREFIX)) {
433            return getSystemColumn(propertyId);
434        } else {
435            if (propertyId.indexOf(':') == -1) {
436                SchemaManager schemaManager = Framework.getService(SchemaManager.class);
437
438                for (Schema schema : schemaManager.getSchemas()) {
439                    if (!schema.getNamespace().hasPrefix()) {
440                        // schema without prefix, try it
441                        if (schema.hasField(propertyId)) {
442                            propertyId = schema.getName() + ":" + propertyId;
443                            break;
444                        }
445                    }
446                }
447            }
448            // CMIS property names are identical to NXQL ones for non-system properties
449            return propertyId;
450        }
451    }
452
453    /**
454     * Finds a NXQL system column from a CMIS system property id.
455     */
456    protected String getSystemColumn(String propertyId) {
457        switch (propertyId) {
458        case PropertyIds.OBJECT_ID:
459            return NXQL.ECM_UUID;
460        case PropertyIds.PARENT_ID:
461        case NuxeoTypeHelper.NX_PARENT_ID:
462            return NXQL.ECM_PARENTID;
463        case NuxeoTypeHelper.NX_PATH_SEGMENT:
464            return NXQL.ECM_NAME;
465        case NuxeoTypeHelper.NX_POS:
466            return NXQL.ECM_POS;
467        case PropertyIds.OBJECT_TYPE_ID:
468            return NXQL.ECM_PRIMARYTYPE;
469        case PropertyIds.SECONDARY_OBJECT_TYPE_IDS:
470        case NuxeoTypeHelper.NX_FACETS:
471            return NXQL.ECM_MIXINTYPE;
472        case PropertyIds.VERSION_LABEL:
473            return NXQL.ECM_VERSIONLABEL;
474        case PropertyIds.IS_LATEST_MAJOR_VERSION:
475            return NXQL.ECM_ISLATESTMAJORVERSION;
476        case PropertyIds.IS_LATEST_VERSION:
477            return NXQL.ECM_ISLATESTVERSION;
478        case NuxeoTypeHelper.NX_ISVERSION:
479            return NXQL.ECM_ISVERSION;
480        case NuxeoTypeHelper.NX_ISCHECKEDIN:
481            return NXQL.ECM_ISCHECKEDIN;
482        case NuxeoTypeHelper.NX_LIFECYCLE_STATE:
483            return NXQL.ECM_LIFECYCLESTATE;
484        case PropertyIds.NAME:
485            return NXQL_DC_TITLE;
486        case PropertyIds.DESCRIPTION:
487            return NXQL_DC_DESCRIPTION;
488        case PropertyIds.CREATED_BY:
489            return NXQL_DC_CREATOR;
490        case PropertyIds.CREATION_DATE:
491            return NXQL_DC_CREATED;
492        case PropertyIds.LAST_MODIFICATION_DATE:
493            return NXQL_DC_MODIFIED;
494        case PropertyIds.LAST_MODIFIED_BY:
495            return NXQL_DC_LAST_CONTRIBUTOR;
496        case PropertyIds.SOURCE_ID:
497            return NXQL_REL_SOURCE;
498        case PropertyIds.TARGET_ID:
499            return NXQL_REL_TARGET;
500        }
501        return null;
502    }
503
504    protected static String cmisToNxqlFulltextQuery(String statement) {
505        // NXQL syntax has implicit AND
506        statement = statement.replace(" and ", " ");
507        statement = statement.replace(" AND ", " ");
508        return statement;
509    }
510
511    /**
512     * Convert an ORDER BY part from CMISQL to NXQL.
513     *
514     * @since 6.0
515     */
516    protected String convertOrderBy(String orderBy, TypeManagerImpl typeManager) {
517        List<String> list = new ArrayList<>(1);
518        for (String order : orderBy.split(",")) {
519            order = order.trim();
520            String lower = order.toLowerCase();
521            String prop;
522            boolean asc;
523            if (lower.endsWith(SPACE_ASC)) {
524                prop = order.substring(0, order.length() - SPACE_ASC.length()).trim();
525                asc = true;
526            } else if (lower.endsWith(SPACE_DESC)) {
527                prop = order.substring(0, order.length() - SPACE_DESC.length()).trim();
528                asc = false;
529            } else {
530                prop = order;
531                asc = true; // default is repository-specific
532            }
533            // assume query name is same as property id
534            String propId = typeManager.getPropertyIdForQueryName(prop);
535            if (propId == null) {
536                throw new CmisInvalidArgumentException("Invalid orderBy: " + orderBy);
537            }
538            String col = getColumn(propId);
539            list.add(asc ? col : (col + " DESC"));
540        }
541        return StringUtils.join(list, ", ");
542    }
543
544    /**
545     * Walker of the WHERE clause that doesn't parse fulltext expressions.
546     */
547    public class AnalyzingWalker extends AbstractPredicateWalker {
548
549        public boolean hasContains;
550
551        @Override
552        public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
553            if (hasContains && Framework.getService(ConfigurationService.class)
554                                        .isBooleanPropertyFalse(NuxeoRepository.RELAX_CMIS_SPEC)) {
555                throw new QueryParseException("At most one CONTAINS() is allowed");
556            }
557            hasContains = true;
558            return null;
559        }
560    }
561
562    /**
563     * Walker of the WHERE clause that generates NXQL.
564     */
565    public class GeneratingWalker extends AbstractPredicateWalker {
566
567        public static final String NX_FULLTEXT_INDEX_PREFIX = "nx:";
568
569        public StringBuilder buf = new StringBuilder();
570
571        @Override
572        public Boolean walkNot(Tree opNode, Tree node) {
573            buf.append("NOT ");
574            walkPredicate(node);
575            return null;
576        }
577
578        @Override
579        public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) {
580            buf.append("(");
581            walkPredicate(leftNode);
582            buf.append(" AND ");
583            walkPredicate(rightNode);
584            buf.append(")");
585            return null;
586        }
587
588        @Override
589        public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) {
590            buf.append("(");
591            walkPredicate(leftNode);
592            buf.append(" OR ");
593            walkPredicate(rightNode);
594            buf.append(")");
595            return null;
596        }
597
598        @Override
599        public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) {
600            walkExpr(leftNode);
601            buf.append(" = ");
602            walkExpr(rightNode);
603            return null;
604        }
605
606        @Override
607        public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) {
608            walkExpr(leftNode);
609            buf.append(" <> ");
610            walkExpr(rightNode);
611            return null;
612        }
613
614        @Override
615        public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) {
616            walkExpr(leftNode);
617            buf.append(" > ");
618            walkExpr(rightNode);
619            return null;
620        }
621
622        @Override
623        public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
624            walkExpr(leftNode);
625            buf.append(" >= ");
626            walkExpr(rightNode);
627            return null;
628        }
629
630        @Override
631        public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) {
632            walkExpr(leftNode);
633            buf.append(" < ");
634            walkExpr(rightNode);
635            return null;
636        }
637
638        @Override
639        public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
640            walkExpr(leftNode);
641            buf.append(" <= ");
642            walkExpr(rightNode);
643            return null;
644        }
645
646        @Override
647        public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) {
648            walkExpr(colNode);
649            buf.append(" IN ");
650            walkExpr(listNode);
651            return null;
652        }
653
654        @Override
655        public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) {
656            walkExpr(colNode);
657            buf.append(" NOT IN ");
658            walkExpr(listNode);
659            return null;
660        }
661
662        @Override
663        public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) {
664            walkAny(colNode, "IN", listNode);
665            return null;
666        }
667
668        @Override
669        public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) {
670            walkAny(colNode, "NOT IN", listNode);
671            return null;
672        }
673
674        @Override
675        public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) {
676            // note that argument order is reversed
677            walkAny(colNode, "=", literalNode);
678            return null;
679        }
680
681        protected void walkAny(Tree colNode, String op, Tree exprNode) {
682            ColumnReference col = getColumnReference(colNode);
683            if (col.getPropertyDefinition().getCardinality() != Cardinality.MULTI) {
684                throw new QueryParseException(
685                        "Cannot use " + op + " ANY with single-valued property: " + col.getPropertyQueryName());
686            }
687            String nxqlCol = (String) col.getInfo();
688            buf.append(nxqlCol);
689            if (!NXQL.ECM_MIXINTYPE.equals(nxqlCol)) {
690                buf.append("/*");
691            }
692            buf.append(' ');
693            buf.append(op);
694            buf.append(' ');
695            walkExpr(exprNode);
696        }
697
698        @Override
699        public Boolean walkIsNull(Tree opNode, Tree colNode) {
700            return walkIsNullOrIsNotNull(colNode, true);
701        }
702
703        @Override
704        public Boolean walkIsNotNull(Tree opNode, Tree colNode) {
705            return walkIsNullOrIsNotNull(colNode, false);
706        }
707
708        protected Boolean walkIsNullOrIsNotNull(Tree colNode, boolean isNull) {
709            ColumnReference col = getColumnReference(colNode);
710            boolean multi = col.getPropertyDefinition().getCardinality() == Cardinality.MULTI;
711            walkExpr(colNode);
712            if (multi) {
713                buf.append("/*");
714            }
715            buf.append(isNull ? " IS NULL" : " IS NOT NULL");
716            return null;
717        }
718
719        @Override
720        public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) {
721            walkExpr(colNode);
722            buf.append(" LIKE ");
723            walkExpr(stringNode);
724            return null;
725        }
726
727        @Override
728        public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) {
729            walkExpr(colNode);
730            buf.append(" NOT LIKE ");
731            walkExpr(stringNode);
732            return null;
733        }
734
735        @Override
736        public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
737            String statement = (String) super.walkString(queryNode);
738            String indexName = NXQL.ECM_FULLTEXT;
739            // micro parsing of the fulltext statement to perform fulltext
740            // search on a non default index
741            if (statement.startsWith(NX_FULLTEXT_INDEX_PREFIX)) {
742                statement = statement.substring(NX_FULLTEXT_INDEX_PREFIX.length());
743                int firstColumnIdx = statement.indexOf(':');
744                if (firstColumnIdx > 0 && firstColumnIdx < statement.length() - 1) {
745                    indexName += '_' + statement.substring(0, firstColumnIdx);
746                    statement = statement.substring(firstColumnIdx + 1);
747                } else {
748                    log.warn(String.format("fail to microparse custom fulltext index:" + " fallback to '%s'",
749                            indexName));
750                }
751            }
752            // CMIS syntax to NXQL syntax
753            statement = cmisToNxqlFulltextQuery(statement);
754            buf.append(indexName);
755            buf.append(" = ");
756            buf.append(NXQL.escapeString(statement));
757            return null;
758        }
759
760        @Override
761        public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) {
762            String id = (String) super.walkString(paramNode);
763            buf.append(NXQL.ECM_PARENTID);
764            buf.append(" = ");
765            buf.append(NXQL.escapeString(id));
766            return null;
767        }
768
769        @Override
770        public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) {
771            String id = (String) super.walkString(paramNode);
772            // don't use ecm:ancestorId because the Elasticsearch converter doesn't understand it
773            // buf.append(NXQL.ECM_ANCESTORID);
774            // buf.append(" = ");
775            // buf.append(NXQL.escapeString(id));
776            String path;
777            DocumentRef docRef = new IdRef(id);
778            if (coreSession.exists(docRef)) {
779                path = coreSession.getDocument(docRef).getPathAsString();
780            } else {
781                // TODO better removal
782                path = "/__NOSUCHPATH__";
783            }
784            buf.append(NXQL.ECM_PATH);
785            buf.append(" STARTSWITH ");
786            buf.append(NXQL.escapeString(path));
787            return null;
788        }
789
790        @Override
791        public Object walkList(Tree node) {
792            buf.append("(");
793            for (int i = 0; i < node.getChildCount(); i++) {
794                if (i != 0) {
795                    buf.append(", ");
796                }
797                Tree child = node.getChild(i);
798                walkExpr(child);
799            }
800            buf.append(")");
801            return null;
802        }
803
804        @Override
805        public Object walkBoolean(Tree node) {
806            Object value = super.walkBoolean(node);
807            buf.append(Boolean.FALSE.equals(value) ? "0" : "1");
808            return null;
809        }
810
811        @Override
812        public Object walkNumber(Tree node) {
813            // Double or Long
814            Number value = (Number) super.walkNumber(node);
815            buf.append(value.toString());
816            return null;
817        }
818
819        @Override
820        public Object walkString(Tree node) {
821            String value = (String) super.walkString(node);
822            buf.append(NXQL.escapeString(value));
823            return null;
824        }
825
826        @Override
827        public Object walkTimestamp(Tree node) {
828            Calendar value = (Calendar) super.walkTimestamp(node);
829            buf.append("TIMESTAMP ");
830            buf.append(QUOTE);
831            buf.append(ISO_DATE_TIME_FORMAT.print(LocalDateTime.fromCalendarFields(value)));
832            buf.append(QUOTE);
833            return null;
834        }
835
836        @Override
837        public Object walkCol(Tree node) {
838            String nxqlCol = (String) getColumnReference(node).getInfo();
839            buf.append(nxqlCol);
840            return null;
841        }
842
843        protected ColumnReference getColumnReference(Tree node) {
844            CmisSelector sel = query.getColumnReference(Integer.valueOf(node.getTokenStartIndex()));
845            if (sel instanceof ColumnReference) {
846                return (ColumnReference) sel;
847            } else {
848                throw new QueryParseException("Cannot use column in WHERE clause: " + sel.getName());
849            }
850        }
851    }
852
853    /**
854     * IterableQueryResult wrapping the one from the NXQL query to turn values into CMIS ones.
855     */
856    // static to avoid keeping the whole QueryMaker in the returned object
857    public static class NXQLtoCMISIterableQueryResult
858            implements IterableQueryResult, Iterator<Map<String, Serializable>> {
859
860        protected IterableQueryResult it;
861
862        protected Iterator<Map<String, Serializable>> iter;
863
864        protected Map<String, String> realColumns;
865
866        protected Map<String, ColumnReference> virtualColumns;
867
868        protected NuxeoCmisService service;
869
870        public NXQLtoCMISIterableQueryResult(IterableQueryResult it, Map<String, String> realColumns,
871                Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) {
872            this.it = it;
873            iter = it.iterator();
874            this.realColumns = realColumns;
875            this.virtualColumns = virtualColumns;
876            this.service = service;
877        }
878
879        @Override
880        public Iterator<Map<String, Serializable>> iterator() {
881            return this;
882        }
883
884        @Override
885        public void close() {
886            it.close();
887        }
888
889        @SuppressWarnings("deprecation")
890        @Override
891        public boolean isLife() {
892            return it.isLife();
893        }
894
895        @Override
896        public boolean mustBeClosed() {
897            return it.mustBeClosed();
898        }
899
900        @Override
901        public long size() {
902            return it.size();
903        }
904
905        @Override
906        public long pos() {
907            return it.pos();
908        }
909
910        @Override
911        public void skipTo(long pos) {
912            it.skipTo(pos);
913        }
914
915        @Override
916        public boolean hasNext() {
917            return iter.hasNext();
918        }
919
920        @Override
921        public void remove() {
922            throw new UnsupportedOperationException();
923        }
924
925        @Override
926        public Map<String, Serializable> next() {
927            // map of NXQL to value
928            Map<String, Serializable> nxqlMap = iter.next();
929            return convertToCMISMap(nxqlMap, realColumns, virtualColumns, service);
930
931        }
932
933    }
934
935    protected static Map<String, Serializable> convertToCMISMap(Map<String, Serializable> nxqlMap,
936            Map<String, String> realColumns, Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) {
937        // find the CMIS keys and values
938        Map<String, Serializable> cmisMap = new HashMap<>();
939        for (Entry<String, String> en : realColumns.entrySet()) {
940            String cmisCol = en.getKey();
941            String nxqlCol = en.getValue();
942            Serializable value = nxqlMap.get(nxqlCol);
943            // type conversion to CMIS values
944            if (value instanceof Long) {
945                value = BigInteger.valueOf(((Long) value).longValue());
946            } else if (value instanceof Integer) {
947                value = BigInteger.valueOf(((Integer) value).intValue());
948            } else if (value instanceof Double) {
949                if (((Double) value).isNaN()) {
950                    value = BigDecimal.ZERO;
951                } else {
952                    value = BigDecimal.valueOf(((Double) value).doubleValue());
953                }
954            } else if (value == null) {
955                // special handling of some columns where NULL means FALSE
956                if (NULL_IS_FALSE_COLUMNS.contains(nxqlCol)) {
957                    value = Boolean.FALSE;
958                }
959            }
960            cmisMap.put(cmisCol, value);
961        }
962
963        // virtual values
964        // map to store actual data for each qualifier
965        Map<String, NuxeoObjectData> datas = null;
966        TypeManagerImpl typeManager = service.getTypeManager();
967        for (Entry<String, ColumnReference> vc : virtualColumns.entrySet()) {
968            String key = vc.getKey();
969            ColumnReference col = vc.getValue();
970            String qual = col.getQualifier();
971            if (col.getPropertyId().equals(PropertyIds.BASE_TYPE_ID)) {
972                // special case, no need to get full Nuxeo Document
973                String typeId = (String) cmisMap.get(PropertyIds.OBJECT_TYPE_ID);
974                if (typeId == null) {
975                    throw new NullPointerException();
976                }
977                TypeDefinitionContainer type = typeManager.getTypeById(typeId);
978                String baseTypeId = type.getTypeDefinition().getBaseTypeId().value();
979                cmisMap.put(key, baseTypeId);
980                continue;
981            }
982            if (datas == null) {
983                datas = new HashMap<>(2);
984            }
985            NuxeoObjectData data = datas.get(qual);
986            if (data == null) {
987                // find main id for this qualifier in the result set
988                // (main id always included in joins)
989                // TODO check what happens if cmis:objectId is aliased
990                String id = (String) cmisMap.get(PropertyIds.OBJECT_ID);
991                try {
992                    // reentrant call to the same session, but the MapMaker
993                    // is only called from the IterableQueryResult in
994                    // queryAndFetch which manipulates no session state
995                    // TODO constructing the DocumentModel (in
996                    // NuxeoObjectData) is expensive, try to get value
997                    // directly
998                    data = service.getObject(service.getNuxeoRepository().getId(), id, null, null, null, null, null,
999                            null, null);
1000                } catch (CmisRuntimeException e) {
1001                    log.error("Cannot get document: " + id, e);
1002                }
1003                datas.put(qual, data);
1004            }
1005            Serializable v;
1006            if (data == null) {
1007                // could not fetch
1008                v = null;
1009            } else {
1010                NuxeoPropertyDataBase<?> pd = data.getProperty(col.getPropertyId());
1011                if (pd == null) {
1012                    v = null;
1013                } else {
1014                    if (pd.getPropertyDefinition().getCardinality() == Cardinality.SINGLE) {
1015                        v = (Serializable) pd.getFirstValue();
1016                    } else {
1017                        v = (Serializable) pd.getValues();
1018                    }
1019                }
1020            }
1021            cmisMap.put(key, v);
1022        }
1023
1024        return cmisMap;
1025    }
1026
1027}