001/*
002 * (C) Copyright 2006-2017 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.CmisSelector;
054import org.apache.chemistry.opencmis.server.support.query.ColumnReference;
055import org.apache.chemistry.opencmis.server.support.query.FunctionReference;
056import org.apache.chemistry.opencmis.server.support.query.FunctionReference.CmisQlFunction;
057import org.apache.chemistry.opencmis.server.support.query.QueryObject;
058import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
059import org.apache.chemistry.opencmis.server.support.query.QueryUtilStrict;
060import org.apache.commons.lang.StringUtils;
061import org.apache.commons.logging.Log;
062import org.apache.commons.logging.LogFactory;
063import org.joda.time.LocalDateTime;
064import org.joda.time.format.DateTimeFormatter;
065import org.joda.time.format.ISODateTimeFormat;
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.LifeCycleConstants;
071import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
072import org.nuxeo.ecm.core.query.QueryParseException;
073import org.nuxeo.ecm.core.query.sql.NXQL;
074import org.nuxeo.runtime.api.Framework;
075import org.nuxeo.runtime.services.config.ConfigurationService;
076
077/**
078 * Transformer of CMISQL queries into NXQL queries.
079 */
080public class CMISQLtoNXQL {
081
082    private static final Log log = LogFactory.getLog(CMISQLtoNXQL.class);
083
084    protected static final String CMIS_PREFIX = "cmis:";
085
086    protected static final String NX_PREFIX = "nuxeo:";
087
088    protected static final String NXQL_DOCUMENT = "Document";
089
090    protected static final String NXQL_RELATION = "Relation";
091
092    protected static final String NXQL_DC_TITLE = "dc:title";
093
094    protected static final String NXQL_DC_DESCRIPTION = "dc:description";
095
096    protected static final String NXQL_DC_CREATOR = "dc:creator";
097
098    protected static final String NXQL_DC_CREATED = "dc:created";
099
100    protected static final String NXQL_DC_MODIFIED = "dc:modified";
101
102    protected static final String NXQL_DC_LAST_CONTRIBUTOR = "dc:lastContributor";
103
104    protected static final String NXQL_REL_SOURCE = "relation:source";
105
106    protected static final String NXQL_REL_TARGET = "relation:target";
107
108    protected static final DateTimeFormatter ISO_DATE_TIME_FORMAT = ISODateTimeFormat.dateTime();
109
110    private static final char QUOTE = '\'';
111
112    private static final String SPACE_ASC = " asc";
113
114    private static final String SPACE_DESC = " desc";
115
116    // list of SQL column where NULL (missing value) should be treated as
117    // Boolean.FALSE in a query result
118    protected static final Set<String> NULL_IS_FALSE_COLUMNS = new HashSet<>(Arrays.asList(NXQL.ECM_ISVERSION,
119            NXQL.ECM_ISLATESTVERSION, NXQL.ECM_ISLATESTMAJORVERSION, NXQL.ECM_ISCHECKEDIN));
120
121    protected final boolean supportsProxies;
122
123    protected Map<String, PropertyDefinition<?>> typeInfo;
124
125    protected CoreSession coreSession;
126
127    // ----- filled during walks of the clauses -----
128
129    protected QueryUtilStrict queryUtil;
130
131    protected QueryObject query;
132
133    protected TypeDefinition fromType;
134
135    protected boolean skipDeleted = true;
136
137    // ----- passed to IterableQueryResult -----
138
139    /** The real columns, CMIS name mapped to NXQL. */
140    protected Map<String, String> realColumns = new LinkedHashMap<>();
141
142    /** The non-real-columns we'll return as well. */
143    protected Map<String, ColumnReference> virtualColumns = new LinkedHashMap<>();
144
145    public CMISQLtoNXQL(boolean supportsProxies) {
146        this.supportsProxies = supportsProxies;
147    }
148
149    /**
150     * Gets the NXQL from a CMISQL query.
151     */
152    public String getNXQL(String cmisql, NuxeoCmisService service, Map<String, PropertyDefinition<?>> typeInfo,
153            boolean searchAllVersions) throws QueryParseException {
154        this.typeInfo = typeInfo;
155        boolean searchLatestVersion = !searchAllVersions;
156        TypeManagerImpl typeManager = service.getTypeManager();
157        coreSession = service.coreSession;
158
159        try {
160            queryUtil = new QueryUtilStrict(cmisql, typeManager, new AnalyzingWalker(), false);
161            queryUtil.processStatement();
162            query = queryUtil.getQueryObject();
163        } catch (RecognitionException e) {
164            throw new QueryParseException(queryUtil.getErrorMessage(e), 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<>();
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<>();
212            if (fromType.getParentTypeId() != null) {
213                // don't add abstract root types
214                types.add(fromType.getId());
215            }
216            LinkedList<TypeDefinitionContainer> typesTodo = new LinkedList<>();
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        if (!supportsProxies) {
259            whereClauses.add(NXQL.ECM_ISPROXY + " = 0");
260        }
261
262        // WHERE clause
263
264        Tree whereNode = queryUtil.getWalker().getWherePredicateTree();
265        if (whereNode != null) {
266            GeneratingWalker generator = new GeneratingWalker();
267            generator.walkPredicate(whereNode);
268            whereClauses.add(generator.buf.toString());
269        }
270
271        // ORDER BY clause
272
273        List<String> orderbys = new ArrayList<>();
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 " + 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(
400                    "Cannot use column in " + clauseType + " clause: " + 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 && Framework.getService(ConfigurationService.class)
532                                        .isBooleanPropertyFalse(NuxeoRepository.RELAX_CMIS_SPEC)) {
533                throw new QueryParseException("At most one CONTAINS() is allowed");
534            }
535            hasContains = true;
536            return null;
537        }
538    }
539
540    /**
541     * Walker of the WHERE clause that generates NXQL.
542     */
543    public class GeneratingWalker extends AbstractPredicateWalker {
544
545        public static final String NX_FULLTEXT_INDEX_PREFIX = "nx:";
546
547        public StringBuilder buf = new StringBuilder();
548
549        @Override
550        public Boolean walkNot(Tree opNode, Tree node) {
551            buf.append("NOT ");
552            walkPredicate(node);
553            return null;
554        }
555
556        @Override
557        public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) {
558            buf.append("(");
559            walkPredicate(leftNode);
560            buf.append(" AND ");
561            walkPredicate(rightNode);
562            buf.append(")");
563            return null;
564        }
565
566        @Override
567        public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) {
568            buf.append("(");
569            walkPredicate(leftNode);
570            buf.append(" OR ");
571            walkPredicate(rightNode);
572            buf.append(")");
573            return null;
574        }
575
576        @Override
577        public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) {
578            walkExpr(leftNode);
579            buf.append(" = ");
580            walkExpr(rightNode);
581            return null;
582        }
583
584        @Override
585        public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) {
586            walkExpr(leftNode);
587            buf.append(" <> ");
588            walkExpr(rightNode);
589            return null;
590        }
591
592        @Override
593        public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) {
594            walkExpr(leftNode);
595            buf.append(" > ");
596            walkExpr(rightNode);
597            return null;
598        }
599
600        @Override
601        public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
602            walkExpr(leftNode);
603            buf.append(" >= ");
604            walkExpr(rightNode);
605            return null;
606        }
607
608        @Override
609        public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) {
610            walkExpr(leftNode);
611            buf.append(" < ");
612            walkExpr(rightNode);
613            return null;
614        }
615
616        @Override
617        public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
618            walkExpr(leftNode);
619            buf.append(" <= ");
620            walkExpr(rightNode);
621            return null;
622        }
623
624        @Override
625        public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) {
626            walkExpr(colNode);
627            buf.append(" IN ");
628            walkExpr(listNode);
629            return null;
630        }
631
632        @Override
633        public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) {
634            walkExpr(colNode);
635            buf.append(" NOT IN ");
636            walkExpr(listNode);
637            return null;
638        }
639
640        @Override
641        public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) {
642            walkAny(colNode, "IN", listNode);
643            return null;
644        }
645
646        @Override
647        public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) {
648            walkAny(colNode, "NOT IN", listNode);
649            return null;
650        }
651
652        @Override
653        public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) {
654            // note that argument order is reversed
655            walkAny(colNode, "=", literalNode);
656            return null;
657        }
658
659        protected void walkAny(Tree colNode, String op, Tree exprNode) {
660            ColumnReference col = getColumnReference(colNode);
661            if (col.getPropertyDefinition().getCardinality() != Cardinality.MULTI) {
662                throw new QueryParseException(
663                        "Cannot use " + op + " ANY with single-valued property: " + col.getPropertyQueryName());
664            }
665            String nxqlCol = (String) col.getInfo();
666            buf.append(nxqlCol);
667            if (!NXQL.ECM_MIXINTYPE.equals(nxqlCol)) {
668                buf.append("/*");
669            }
670            buf.append(' ');
671            buf.append(op);
672            buf.append(' ');
673            walkExpr(exprNode);
674        }
675
676        @Override
677        public Boolean walkIsNull(Tree opNode, Tree colNode) {
678            return walkIsNullOrIsNotNull(colNode, true);
679        }
680
681        @Override
682        public Boolean walkIsNotNull(Tree opNode, Tree colNode) {
683            return walkIsNullOrIsNotNull(colNode, false);
684        }
685
686        protected Boolean walkIsNullOrIsNotNull(Tree colNode, boolean isNull) {
687            ColumnReference col = getColumnReference(colNode);
688            boolean multi = col.getPropertyDefinition().getCardinality() == Cardinality.MULTI;
689            walkExpr(colNode);
690            if (multi) {
691                buf.append("/*");
692            }
693            buf.append(isNull ? " IS NULL" : " IS NOT NULL");
694            return null;
695        }
696
697        @Override
698        public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) {
699            walkExpr(colNode);
700            buf.append(" LIKE ");
701            walkExpr(stringNode);
702            return null;
703        }
704
705        @Override
706        public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) {
707            walkExpr(colNode);
708            buf.append(" NOT LIKE ");
709            walkExpr(stringNode);
710            return null;
711        }
712
713        @Override
714        public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
715            String statement = (String) super.walkString(queryNode);
716            String indexName = NXQL.ECM_FULLTEXT;
717            // micro parsing of the fulltext statement to perform fulltext
718            // search on a non default index
719            if (statement.startsWith(NX_FULLTEXT_INDEX_PREFIX)) {
720                statement = statement.substring(NX_FULLTEXT_INDEX_PREFIX.length());
721                int firstColumnIdx = statement.indexOf(':');
722                if (firstColumnIdx > 0 && firstColumnIdx < statement.length() - 1) {
723                    indexName += '_' + statement.substring(0, firstColumnIdx);
724                    statement = statement.substring(firstColumnIdx + 1);
725                } else {
726                    log.warn(String.format("fail to microparse custom fulltext index:" + " fallback to '%s'",
727                            indexName));
728                }
729            }
730            // CMIS syntax to NXQL syntax
731            statement = cmisToNxqlFulltextQuery(statement);
732            buf.append(indexName);
733            buf.append(" = ");
734            buf.append(NXQL.escapeString(statement));
735            return null;
736        }
737
738        @Override
739        public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) {
740            String id = (String) super.walkString(paramNode);
741            buf.append(NXQL.ECM_PARENTID);
742            buf.append(" = ");
743            buf.append(NXQL.escapeString(id));
744            return null;
745        }
746
747        @Override
748        public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) {
749            String id = (String) super.walkString(paramNode);
750            // don't use ecm:ancestorId because the Elasticsearch converter doesn't understand it
751            // buf.append(NXQL.ECM_ANCESTORID);
752            // buf.append(" = ");
753            // buf.append(NXQL.escapeString(id));
754            String path;
755            DocumentRef docRef = new IdRef(id);
756            if (coreSession.exists(docRef)) {
757                path = coreSession.getDocument(docRef).getPathAsString();
758            } else {
759                // TODO better removal
760                path = "/__NOSUCHPATH__";
761            }
762            buf.append(NXQL.ECM_PATH);
763            buf.append(" STARTSWITH ");
764            buf.append(NXQL.escapeString(path));
765            return null;
766        }
767
768        @Override
769        public Object walkList(Tree node) {
770            buf.append("(");
771            for (int i = 0; i < node.getChildCount(); i++) {
772                if (i != 0) {
773                    buf.append(", ");
774                }
775                Tree child = node.getChild(i);
776                walkExpr(child);
777            }
778            buf.append(")");
779            return null;
780        }
781
782        @Override
783        public Object walkBoolean(Tree node) {
784            Object value = super.walkBoolean(node);
785            buf.append(Boolean.FALSE.equals(value) ? "0" : "1");
786            return null;
787        }
788
789        @Override
790        public Object walkNumber(Tree node) {
791            // Double or Long
792            Number value = (Number) super.walkNumber(node);
793            buf.append(value.toString());
794            return null;
795        }
796
797        @Override
798        public Object walkString(Tree node) {
799            String value = (String) super.walkString(node);
800            buf.append(NXQL.escapeString(value));
801            return null;
802        }
803
804        @Override
805        public Object walkTimestamp(Tree node) {
806            Calendar value = (Calendar) super.walkTimestamp(node);
807            buf.append("TIMESTAMP ");
808            buf.append(QUOTE);
809            buf.append(ISO_DATE_TIME_FORMAT.print(LocalDateTime.fromCalendarFields(value)));
810            buf.append(QUOTE);
811            return null;
812        }
813
814        @Override
815        public Object walkCol(Tree node) {
816            String nxqlCol = (String) getColumnReference(node).getInfo();
817            buf.append(nxqlCol);
818            return null;
819        }
820
821        protected ColumnReference getColumnReference(Tree node) {
822            CmisSelector sel = query.getColumnReference(Integer.valueOf(node.getTokenStartIndex()));
823            if (sel instanceof ColumnReference) {
824                return (ColumnReference) sel;
825            } else {
826                throw new QueryParseException("Cannot use column in WHERE clause: " + sel.getName());
827            }
828        }
829    }
830
831    /**
832     * IterableQueryResult wrapping the one from the NXQL query to turn values into CMIS ones.
833     */
834    // static to avoid keeping the whole QueryMaker in the returned object
835    public static class NXQLtoCMISIterableQueryResult
836            implements IterableQueryResult, Iterator<Map<String, Serializable>> {
837
838        protected IterableQueryResult it;
839
840        protected Iterator<Map<String, Serializable>> iter;
841
842        protected Map<String, String> realColumns;
843
844        protected Map<String, ColumnReference> virtualColumns;
845
846        protected NuxeoCmisService service;
847
848        public NXQLtoCMISIterableQueryResult(IterableQueryResult it, Map<String, String> realColumns,
849                Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) {
850            this.it = it;
851            iter = it.iterator();
852            this.realColumns = realColumns;
853            this.virtualColumns = virtualColumns;
854            this.service = service;
855        }
856
857        @Override
858        public Iterator<Map<String, Serializable>> iterator() {
859            return this;
860        }
861
862        @Override
863        public void close() {
864            it.close();
865        }
866
867        @SuppressWarnings("deprecation")
868        @Override
869        public boolean isLife() {
870            return it.isLife();
871        }
872
873        @Override
874        public boolean mustBeClosed() {
875            return it.mustBeClosed();
876        }
877
878        @Override
879        public long size() {
880            return it.size();
881        }
882
883        @Override
884        public long pos() {
885            return it.pos();
886        }
887
888        @Override
889        public void skipTo(long pos) {
890            it.skipTo(pos);
891        }
892
893        @Override
894        public boolean hasNext() {
895            return iter.hasNext();
896        }
897
898        @Override
899        public void remove() {
900            throw new UnsupportedOperationException();
901        }
902
903        @Override
904        public Map<String, Serializable> next() {
905            // map of NXQL to value
906            Map<String, Serializable> nxqlMap = iter.next();
907
908            // find the CMIS keys and values
909            Map<String, Serializable> cmisMap = new HashMap<>();
910            for (Entry<String, String> en : realColumns.entrySet()) {
911                String cmisCol = en.getKey();
912                String nxqlCol = en.getValue();
913                Serializable value = nxqlMap.get(nxqlCol);
914                // type conversion to CMIS values
915                if (value instanceof Long) {
916                    value = BigInteger.valueOf(((Long) value).longValue());
917                } else if (value instanceof Integer) {
918                    value = BigInteger.valueOf(((Integer) value).intValue());
919                } else if (value instanceof Double) {
920                    value = BigDecimal.valueOf(((Double) value).doubleValue());
921                } else if (value == null) {
922                    // special handling of some columns where NULL means FALSE
923                    if (NULL_IS_FALSE_COLUMNS.contains(nxqlCol)) {
924                        value = Boolean.FALSE;
925                    }
926                }
927                cmisMap.put(cmisCol, value);
928            }
929
930            // virtual values
931            // map to store actual data for each qualifier
932            Map<String, NuxeoObjectData> datas = null;
933            TypeManagerImpl typeManager = service.getTypeManager();
934            for (Entry<String, ColumnReference> vc : virtualColumns.entrySet()) {
935                String key = vc.getKey();
936                ColumnReference col = vc.getValue();
937                String qual = col.getQualifier();
938                if (col.getPropertyId().equals(PropertyIds.BASE_TYPE_ID)) {
939                    // special case, no need to get full Nuxeo Document
940                    String typeId = (String) cmisMap.get(PropertyIds.OBJECT_TYPE_ID);
941                    if (typeId == null) {
942                        throw new NullPointerException();
943                    }
944                    TypeDefinitionContainer type = typeManager.getTypeById(typeId);
945                    String baseTypeId = type.getTypeDefinition().getBaseTypeId().value();
946                    cmisMap.put(key, baseTypeId);
947                    continue;
948                }
949                if (datas == null) {
950                    datas = new HashMap<>(2);
951                }
952                NuxeoObjectData data = datas.get(qual);
953                if (data == null) {
954                    // find main id for this qualifier in the result set
955                    // (main id always included in joins)
956                    // TODO check what happens if cmis:objectId is aliased
957                    String id = (String) cmisMap.get(PropertyIds.OBJECT_ID);
958                    try {
959                        // reentrant call to the same session, but the MapMaker
960                        // is only called from the IterableQueryResult in
961                        // queryAndFetch which manipulates no session state
962                        // TODO constructing the DocumentModel (in
963                        // NuxeoObjectData) is expensive, try to get value
964                        // directly
965                        data = (NuxeoObjectData) service.getObject(service.getNuxeoRepository().getId(), id, null, null,
966                                null, null, null, null, null);
967                    } catch (CmisRuntimeException e) {
968                        log.error("Cannot get document: " + id, e);
969                    }
970                    datas.put(qual, data);
971                }
972                Serializable v;
973                if (data == null) {
974                    // could not fetch
975                    v = null;
976                } else {
977                    NuxeoPropertyDataBase<?> pd = (NuxeoPropertyDataBase<?>) data.getProperty(col.getPropertyId());
978                    if (pd == null) {
979                        v = null;
980                    } else {
981                        if (pd.getPropertyDefinition().getCardinality() == Cardinality.SINGLE) {
982                            v = (Serializable) pd.getFirstValue();
983                        } else {
984                            v = (Serializable) pd.getValues();
985                        }
986                    }
987                }
988                cmisMap.put(key, v);
989            }
990
991            return cmisMap;
992        }
993    }
994
995}