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