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