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