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