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