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