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