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