001/*
002 * (C) Copyright 2006-2017 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.opencmis.impl.server;
020
021import java.io.Serializable;
022import java.math.BigDecimal;
023import java.math.BigInteger;
024import java.security.Principal;
025import java.sql.ResultSet;
026import java.sql.SQLException;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.Iterator;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036import java.util.Map.Entry;
037import java.util.Set;
038import java.util.TreeSet;
039import java.util.concurrent.atomic.AtomicInteger;
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.CmisRuntimeException;
050import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalDefinitionImpl;
051import org.apache.chemistry.opencmis.server.support.query.AbstractPredicateWalker;
052import org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer;
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.QueryUtilStrict;
059import org.apache.chemistry.opencmis.server.support.query.QueryObject.JoinSpec;
060import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
061import org.apache.commons.lang.StringUtils;
062import org.apache.commons.logging.Log;
063import org.apache.commons.logging.LogFactory;
064import org.nuxeo.ecm.core.api.LifeCycleConstants;
065import org.nuxeo.ecm.core.api.security.SecurityConstants;
066import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
067import org.nuxeo.ecm.core.query.QueryFilter;
068import org.nuxeo.ecm.core.query.QueryParseException;
069import org.nuxeo.ecm.core.schema.FacetNames;
070import org.nuxeo.ecm.core.security.SecurityPolicy;
071import org.nuxeo.ecm.core.security.SecurityPolicy.QueryTransformer;
072import org.nuxeo.ecm.core.security.SecurityPolicyService;
073import org.nuxeo.ecm.core.storage.sql.Model;
074import org.nuxeo.ecm.core.storage.sql.ModelProperty;
075import org.nuxeo.ecm.core.storage.sql.Session.PathResolver;
076import org.nuxeo.ecm.core.storage.sql.jdbc.QueryMaker;
077import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo;
078import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.MapMaker;
079import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.SQLInfoSelect;
080import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
081import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database;
082import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select;
083import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
084import org.nuxeo.ecm.core.storage.sql.jdbc.db.TableAlias;
085import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
086import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect.FulltextMatchInfo;
087import org.nuxeo.runtime.api.Framework;
088import org.nuxeo.runtime.services.config.ConfigurationService;
089
090/**
091 * Transformer of CMISQL queries into real SQL queries for the actual database.
092 */
093public class CMISQLQueryMaker implements QueryMaker {
094
095    private static final Log log = LogFactory.getLog(CMISQLQueryMaker.class);
096
097    public static final String TYPE = "CMISQL";
098
099    public static final String CMIS_PREFIX = "cmis:";
100
101    public static final String NX_PREFIX = "nuxeo:";
102
103    public static final String DC_FRAGMENT_NAME = "dublincore";
104
105    public static final String DC_TITLE_KEY = "title";
106
107    public static final String DC_DESCRIPTION_KEY = "description";
108
109    public static final String DC_CREATOR_KEY = "creator";
110
111    public static final String DC_CREATED_KEY = "created";
112
113    public static final String DC_MODIFIED_KEY = "modified";
114
115    public static final String DC_LAST_CONTRIBUTOR_KEY = "lastContributor";
116
117    public static final String REL_FRAGMENT_NAME = "relation";
118
119    public static final String REL_SOURCE_KEY = "source";
120
121    public static final String REL_TARGET_KEY = "target";
122
123    // list of SQL column where NULL (missing value) should be treated as
124    // Boolean.FALSE
125    public static final Set<String> NULL_IS_FALSE_COLUMNS = new HashSet<>(
126            Arrays.asList(Model.HIER_TABLE_NAME + " " + Model.MAIN_IS_VERSION_KEY,
127                    Model.VERSION_TABLE_NAME + " " + Model.VERSION_IS_LATEST_KEY,
128                    Model.VERSION_TABLE_NAME + " " + Model.VERSION_IS_LATEST_MAJOR_KEY,
129                    Model.HIER_TABLE_NAME + " " + Model.MAIN_CHECKED_IN_KEY));
130
131    /**
132     * These mixins never match an instance mixin when used in a clause nuxeo:secondaryObjectTypeIds = 'foo'
133     */
134    protected static final Set<String> MIXINS_NOT_PER_INSTANCE = new HashSet<>(
135            Arrays.asList(FacetNames.FOLDERISH, FacetNames.HIDDEN_IN_NAVIGATION));
136
137    protected Database database;
138
139    protected Dialect dialect;
140
141    protected Model model;
142
143    protected Table hierTable;
144
145    public boolean skipDeleted = true;
146
147    // ----- filled during walks of the clauses -----
148
149    protected QueryUtilStrict queryUtil;
150
151    protected QueryObject query;
152
153    protected FulltextMatchInfo fulltextMatchInfo;
154
155    protected Set<String> lifecycleWhereClauseQualifiers = new HashSet<>();
156
157    protected Set<String> mixinTypeWhereClauseQualifiers = new HashSet<>();
158
159    /** Qualifier to type. */
160    protected Map<String, String> qualifierToType = new HashMap<>();
161
162    /** Qualifier to canonical qualifier (correlation name). */
163    protected Map<String, String> canonicalQualifier = new HashMap<>();
164
165    /** Map of qualifier -> fragment -> table */
166    protected Map<String, Map<String, Table>> allTables = new HashMap<>();
167
168    /** All qualifiers used (includes virtual columns) */
169    protected Set<String> allQualifiers = new HashSet<>();
170
171    /** The qualifiers which correspond to versionable types. */
172    protected Set<String> versionableQualifiers = new HashSet<>();
173
174    /** The columns we'll actually request from the database. */
175    protected List<SqlColumn> realColumns = new LinkedList<>();
176
177    /** Parameters for above (for SCORE expressions on some databases) */
178    protected List<String> realColumnsParams = new LinkedList<>();
179
180    /** The non-real-columns we'll return as well. */
181    protected Map<String, ColumnReference> virtualColumns = new HashMap<>();
182
183    /** Type info returned to caller. */
184    protected Map<String, PropertyDefinition<?>> typeInfo = null;
185
186    /** Search only latest version = !searchAllVersions. */
187    protected boolean searchLatestVersion = false;
188
189    /** used for diagnostic when using DISTINCT */
190    protected List<String> virtualColumnNames = new LinkedList<>();
191
192    /**
193     * Column corresponding to a returned value computed from an actual SQL expression.
194     */
195    public static class SqlColumn {
196
197        /** Column name or expression passed to SQL statement. */
198        public final String sql;
199
200        /** Column used to get the value from the result set. */
201        public final Column column;
202
203        /** Key for the value returned to the caller. */
204        public final String key;
205
206        public SqlColumn(String sql, Column column, String key) {
207            this.sql = sql;
208            this.column = column;
209            this.key = key;
210        }
211    }
212
213    @Override
214    public String getName() {
215        return TYPE;
216    }
217
218    @Override
219    public boolean accepts(String queryType) {
220        return queryType.equals(TYPE);
221    }
222
223    /**
224     * {@inheritDoc}
225     * <p>
226     * The optional parameters must be passed: {@code params[0]} is the {@link NuxeoCmisService}, optional
227     * {@code params[1]} is a type info map, optional {@code params[2]} is searchAllVersions (default
228     * {@code Boolean.TRUE} for this method).
229     */
230    @Override
231    public Query buildQuery(SQLInfo sqlInfo, Model model, PathResolver pathResolver, String statement,
232            QueryFilter queryFilter, Object... params) {
233        database = sqlInfo.database;
234        dialect = sqlInfo.dialect;
235        this.model = model;
236        NuxeoCmisService service = (NuxeoCmisService) params[0];
237        if (params.length > 1) {
238            typeInfo = (Map<String, PropertyDefinition<?>>) params[1];
239        }
240        if (params.length > 2) {
241            Boolean searchAllVersions = (Boolean) params[2];
242            searchLatestVersion = Boolean.FALSE.equals(searchAllVersions);
243        }
244        TypeManagerImpl typeManager = service.getTypeManager();
245
246        boolean addSystemColumns = true; // TODO
247
248        hierTable = database.getTable(Model.HIER_TABLE_NAME);
249
250        statement = applySecurityPolicyQueryTransformers(service, queryFilter.getPrincipal(), statement);
251        try {
252            queryUtil = new QueryUtilStrict(statement, typeManager, new AnalyzingWalker(), false);
253            queryUtil.processStatement();
254            query = queryUtil.getQueryObject();
255        } catch (RecognitionException e) {
256            throw new QueryParseException(queryUtil.getErrorMessage(e), e);
257        }
258
259        resolveQualifiers();
260
261        // now resolve column selectors to actual database columns
262        for (CmisSelector sel : query.getSelectReferences()) {
263            recordSelectSelector(sel);
264        }
265        for (CmisSelector sel : query.getJoinReferences()) {
266            recordSelector(sel, JOIN);
267        }
268        for (CmisSelector sel : query.getWhereReferences()) {
269            recordSelector(sel, WHERE);
270        }
271        for (SortSpec spec : query.getOrderBys()) {
272            recordSelector(spec.getSelector(), ORDER_BY);
273        }
274
275        findVersionableQualifiers();
276
277        boolean distinct = false; // TODO extension
278        addSystemColumns(addSystemColumns, distinct);
279
280        /*
281         * Find info about fragments needed.
282         */
283
284        List<String> whereClauses = new LinkedList<String>();
285        List<Serializable> whereParams = new LinkedList<Serializable>();
286
287        /*
288         * Walk joins.
289         */
290
291        List<JoinSpec> joins = query.getJoins();
292        StringBuilder from = new StringBuilder();
293        List<Serializable> fromParams = new LinkedList<Serializable>();
294        for (int njoin = -1; njoin < joins.size(); njoin++) {
295            JoinSpec join;
296            boolean outerJoin;
297            String alias;
298            if (njoin == -1) {
299                join = null;
300                outerJoin = false;
301                alias = query.getMainTypeAlias();
302            } else {
303                join = joins.get(njoin);
304                outerJoin = join.kind.equals("LEFT") || join.kind.equals("RIGHT");
305                alias = join.alias;
306            }
307
308            String typeQueryName = qualifierToType.get(alias);
309            String qual = canonicalQualifier.get(alias);
310            Table qualHierTable = getTable(hierTable, qual);
311
312            // determine relevant primary types
313
314            List<String> types = new ArrayList<String>();
315            TypeDefinition td = query.getTypeDefinitionFromQueryName(typeQueryName);
316            if (td.getParentTypeId() != null) {
317                // don't add abstract root types
318                types.add(td.getId());
319            }
320            LinkedList<TypeDefinitionContainer> typesTodo = new LinkedList<TypeDefinitionContainer>();
321            typesTodo.addAll(typeManager.getTypeDescendants(td.getId(), -1, Boolean.TRUE));
322            // recurse to get all subtypes
323            TypeDefinitionContainer tc;
324            while ((tc = typesTodo.poll()) != null) {
325                types.add(tc.getTypeDefinition().getId());
326                typesTodo.addAll(tc.getChildren());
327            }
328            if (types.isEmpty()) {
329                // shoudn't happen
330                types = Collections.singletonList("__NOSUCHTYPE__");
331            }
332            // build clause
333            StringBuilder qms = new StringBuilder();
334            for (int i = 0; i < types.size(); i++) {
335                if (i != 0) {
336                    qms.append(", ");
337                }
338                qms.append("?");
339            }
340            String primaryTypeClause = String.format("%s IN (%s)",
341                    qualHierTable.getColumn(Model.MAIN_PRIMARY_TYPE_KEY).getFullQuotedName(), qms);
342
343            // table this join is about
344
345            Table table;
346            if (join == null) {
347                table = qualHierTable;
348            } else {
349                // find which table in onLeft/onRight refers to current
350                // qualifier
351                table = null;
352                for (ColumnReference col : Arrays.asList(join.onLeft, join.onRight)) {
353                    if (alias.equals(col.getQualifier())) {
354                        // TODO match with canonical qualifier instead?
355                        table = ((Column) col.getInfo()).getTable();
356                        break;
357                    }
358                }
359                if (table == null) {
360                    throw new QueryParseException("Bad query, qualifier not found: " + qual);
361                }
362            }
363            String tableName;
364            if (table.isAlias()) {
365                tableName = table.getRealTable().getQuotedName() + " " + table.getQuotedName();
366            } else {
367                tableName = table.getQuotedName();
368            }
369            boolean isRelation = table.getKey().equals(REL_FRAGMENT_NAME);
370
371            // join clause on requested columns
372
373            boolean primaryTypeClauseDone = false;
374
375            if (join == null) {
376                from.append(tableName);
377            } else {
378                if (outerJoin) {
379                    from.append(" ");
380                    from.append(join.kind);
381                }
382                from.append(" JOIN ");
383                from.append(tableName);
384                from.append(" ON (");
385                from.append(((Column) join.onLeft.getInfo()).getFullQuotedName());
386                from.append(" = ");
387                from.append(((Column) join.onRight.getInfo()).getFullQuotedName());
388                if (outerJoin && table.getKey().equals(Model.HIER_TABLE_NAME)) {
389                    // outer join, type check must be part of JOIN
390                    from.append(" AND ");
391                    from.append(primaryTypeClause);
392                    fromParams.addAll(types);
393                    primaryTypeClauseDone = true;
394                }
395                from.append(")");
396            }
397
398            // join other fragments for qualifier
399
400            String tableMainId = table.getColumn(Model.MAIN_KEY).getFullQuotedName();
401
402            for (Table t : allTables.get(qual).values()) {
403                if (t.getKey().equals(table.getKey())) {
404                    // already done above
405                    continue;
406                }
407                String n;
408                if (t.isAlias()) {
409                    n = t.getRealTable().getQuotedName() + " " + t.getQuotedName();
410                } else {
411                    n = t.getQuotedName();
412                }
413                from.append(" LEFT JOIN ");
414                from.append(n);
415                from.append(" ON (");
416                from.append(t.getColumn(Model.MAIN_KEY).getFullQuotedName());
417                from.append(" = ");
418                from.append(tableMainId);
419                if (outerJoin && t.getKey().equals(Model.HIER_TABLE_NAME)) {
420                    // outer join, type check must be part of JOIN
421                    from.append(" AND ");
422                    from.append(primaryTypeClause);
423                    fromParams.addAll(types);
424                    primaryTypeClauseDone = true;
425                }
426                from.append(")");
427            }
428
429            // primary type clause, if not included in a JOIN
430
431            if (!primaryTypeClauseDone) {
432                whereClauses.add(primaryTypeClause);
433                whereParams.addAll(types);
434            }
435
436            // lifecycle not deleted filter
437
438            if (skipDeleted) {
439                ModelProperty propertyInfo = model.getPropertyInfo(Model.MISC_LIFECYCLE_STATE_PROP);
440                Column lscol = getTable(database.getTable(propertyInfo.fragmentName), qual).getColumn(
441                        propertyInfo.fragmentKey);
442                String lscolName = lscol.getFullQuotedName();
443                whereClauses.add(String.format("(%s <> ? OR %s IS NULL)", lscolName, lscolName));
444                whereParams.add(LifeCycleConstants.DELETED_STATE);
445            }
446
447            // searchAllVersions filter
448
449            boolean versionable = versionableQualifiers.contains(qual);
450            if (searchLatestVersion && versionable) {
451                // add islatestversion = true
452                Table ver = getTable(database.getTable(Model.VERSION_TABLE_NAME), qual);
453                Column latestvercol = ver.getColumn(Model.VERSION_IS_LATEST_KEY);
454                String latestvercolName = latestvercol.getFullQuotedName();
455                whereClauses.add(String.format("(%s = ?)", latestvercolName));
456                whereParams.add(Boolean.TRUE);
457            }
458
459            // security check
460
461            boolean checkSecurity = !isRelation //
462                    && queryFilter != null && queryFilter.getPrincipals() != null;
463            if (checkSecurity) {
464                Serializable principals;
465                Serializable permissions;
466                if (dialect.supportsArrays()) {
467                    principals = queryFilter.getPrincipals();
468                    permissions = queryFilter.getPermissions();
469                } else {
470                    principals = StringUtils.join(queryFilter.getPrincipals(), '|');
471                    permissions = StringUtils.join(queryFilter.getPermissions(), '|');
472                }
473                if (dialect.supportsReadAcl()) {
474                    /* optimized read acl */
475                    String readAclTable;
476                    String readAclTableAlias;
477                    String aclrumTable;
478                    String aclrumTableAlias;
479                    if (joins.size() == 0) {
480                        readAclTable = Model.HIER_READ_ACL_TABLE_NAME;
481                        readAclTableAlias = readAclTable;
482                        aclrumTable = Model.ACLR_USER_MAP_TABLE_NAME;
483                        aclrumTableAlias = aclrumTable;
484                    } else {
485                        readAclTableAlias = "nxr" + (njoin + 1);
486                        readAclTable = Model.HIER_READ_ACL_TABLE_NAME + ' ' + readAclTableAlias; // TODO dialect
487                        aclrumTableAlias = "aclrum" + (njoin + 1);
488                        aclrumTable = Model.ACLR_USER_MAP_TABLE_NAME + ' ' + aclrumTableAlias; // TODO dialect
489                    }
490                    String readAclIdCol = readAclTableAlias + '.' + Model.HIER_READ_ACL_ID;
491                    String readAclAclIdCol = readAclTableAlias + '.' + Model.HIER_READ_ACL_ACL_ID;
492                    String aclrumAclIdCol = aclrumTableAlias + '.' + Model.ACLR_USER_MAP_ACL_ID;
493                    String aclrumUserIdCol = aclrumTableAlias + '.' + Model.ACLR_USER_MAP_USER_ID;
494                    // first join with hierarchy_read_acl
495                    if (outerJoin) {
496                        from.append(" ");
497                        from.append(join.kind);
498                    }
499                    from.append(String.format(" JOIN %s ON (%s = %s)", readAclTable, tableMainId, readAclIdCol));
500                    // second join with aclr_user_map
501                    String securityCheck = dialect.getReadAclsCheckSql(aclrumUserIdCol);
502                    String joinOn = String.format("%s = %s", readAclAclIdCol, aclrumAclIdCol);
503                    if (outerJoin) {
504                        from.append(" ");
505                        from.append(join.kind);
506                        // outer join, security check must be part of JOIN
507                        joinOn = String.format("%s AND %s", joinOn, securityCheck);
508                        fromParams.add(principals);
509                    } else {
510                        // inner join, security check can go in WHERE clause
511                        whereClauses.add(securityCheck);
512                        whereParams.add(principals);
513                    }
514                    from.append(String.format(" JOIN %s ON (%s)", aclrumTable, joinOn));
515                } else {
516                    String securityCheck = dialect.getSecurityCheckSql(tableMainId);
517                    if (outerJoin) {
518                        securityCheck = String.format("(%s OR %s IS NULL)", securityCheck, tableMainId);
519                    }
520                    whereClauses.add(securityCheck);
521                    whereParams.add(principals);
522                    whereParams.add(permissions);
523                }
524            }
525        }
526
527        /*
528         * WHERE clause.
529         */
530
531        Tree whereNode = queryUtil.getWalker().getWherePredicateTree();
532        if (whereNode != null) {
533            GeneratingWalker generator = new GeneratingWalker();
534            generator.walkPredicate(whereNode);
535            whereClauses.add(generator.whereBuf.toString());
536            whereParams.addAll(generator.whereBufParams);
537
538            // add JOINs for the external fulltext matches
539            Collections.sort(generator.ftJoins); // implicit JOINs last
540                                                 // (PostgreSQL)
541            for (org.nuxeo.ecm.core.storage.sql.jdbc.db.Join join : generator.ftJoins) {
542                from.append(join.toSql(dialect));
543                if (join.tableParam != null) {
544                    fromParams.add(join.tableParam);
545                }
546            }
547        }
548
549        /*
550         * SELECT clause.
551         */
552
553        List<String> selectWhat = new ArrayList<String>();
554        List<Serializable> selectParams = new ArrayList<Serializable>(1);
555        for (SqlColumn rc : realColumns) {
556            selectWhat.add(rc.sql);
557        }
558        selectParams.addAll(realColumnsParams);
559
560        CMISQLMapMaker mapMaker = new CMISQLMapMaker(realColumns, virtualColumns, service);
561        String what = StringUtils.join(selectWhat, ", ");
562        if (distinct) {
563            what = "DISTINCT " + what;
564        }
565
566        /*
567         * ORDER BY clause.
568         */
569
570        List<String> orderbys = new LinkedList<String>();
571        for (SortSpec spec : query.getOrderBys()) {
572            String orderby;
573            CmisSelector sel = spec.getSelector();
574            if (sel instanceof ColumnReference) {
575                Column column = (Column) sel.getInfo();
576                orderby = column.getFullQuotedName();
577            } else {
578                orderby = fulltextMatchInfo.scoreAlias;
579            }
580            if (!spec.ascending) {
581                orderby += " DESC";
582            }
583            orderbys.add(orderby);
584        }
585
586        /*
587         * Create the whole select.
588         */
589
590        Select select = new Select(null);
591        select.setWhat(what);
592        select.setFrom(from.toString());
593        // TODO(fromParams); // TODO add before whereParams
594        select.setWhere(StringUtils.join(whereClauses, " AND "));
595        select.setOrderBy(StringUtils.join(orderbys, ", "));
596
597        Query q = new Query();
598        q.selectInfo = new SQLInfoSelect(select.getStatement(), mapMaker);
599        q.selectParams = selectParams;
600        q.selectParams.addAll(fromParams);
601        q.selectParams.addAll(whereParams);
602        return q;
603    }
604
605    /**
606     * Applies security policies query transformers to the statement, if possible. Otherwise raises an exception.
607     *
608     * @since 5.7.2
609     * @throws CmisRuntimeException If a security policy prevents doing CMIS queries.
610     */
611    protected String applySecurityPolicyQueryTransformers(NuxeoCmisService service, Principal principal,
612            String statement) {
613        SecurityPolicyService securityPolicyService = Framework.getLocalService(SecurityPolicyService.class);
614        if (securityPolicyService == null) {
615            return statement;
616        }
617        String repositoryId = service.getNuxeoRepository().getId();
618        for (SecurityPolicy policy : securityPolicyService.getPolicies()) {
619            if (!policy.isRestrictingPermission(SecurityConstants.BROWSE)) {
620                continue;
621            }
622            // check CMISQL transformer (new @since 5.7.2)
623            if (!policy.isExpressibleInQuery(repositoryId, TYPE)) {
624                throw new CmisRuntimeException(
625                        "Security policy " + policy.getClass().getName() + " prevents CMISQL execution");
626            }
627            QueryTransformer transformer = policy.getQueryTransformer(repositoryId, TYPE);
628            statement = transformer.transform(principal, statement);
629        }
630        return statement;
631    }
632
633    protected void findVersionableQualifiers() {
634        List<JoinSpec> joins = query.getJoins();
635        for (int njoin = -1; njoin < joins.size(); njoin++) {
636            boolean firstTable = njoin == -1;
637            String alias;
638            if (firstTable) {
639                alias = query.getMainTypeAlias();
640            } else {
641                alias = joins.get(njoin).alias;
642            }
643            String typeQueryName = qualifierToType.get(alias);
644            TypeDefinition td = query.getTypeDefinitionFromQueryName(typeQueryName);
645            boolean versionable = td.getBaseTypeId() == BaseTypeId.CMIS_DOCUMENT;
646            if (versionable) {
647                String qual = canonicalQualifier.get(alias);
648                versionableQualifiers.add(qual);
649            }
650        }
651    }
652
653    protected boolean isFacetsColumn(String name) {
654        return PropertyIds.SECONDARY_OBJECT_TYPE_IDS.equals(name) || NuxeoTypeHelper.NX_FACETS.equals(name);
655    }
656
657    // add main id to all qualifiers if
658    // - we have no DISTINCT (in which case more columns don't matter), or
659    // - we have virtual columns, or
660    // - system columns are requested
661    // check no added columns would bias the DISTINCT
662    // after this method, allTables also contain hier table for virtual columns
663    protected void addSystemColumns(boolean addSystemColumns, boolean distinct) {
664
665        List<CmisSelector> addedSystemColumns = new ArrayList<CmisSelector>(2);
666
667        for (String qual : allQualifiers) {
668            TypeDefinition type = getTypeForQualifier(qual);
669
670            // additional references to cmis:objectId and cmis:objectTypeId
671            for (String propertyId : Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID)) {
672                ColumnReference col = new ColumnReference(qual, propertyId);
673                col.setTypeDefinition(propertyId, type);
674                String key = getColumnKey(col);
675                boolean add = true;
676                for (SqlColumn rc : realColumns) {
677                    if (rc.key.equals(key)) {
678                        add = false;
679                        break;
680                    }
681                }
682                if (add) {
683                    addedSystemColumns.add(col);
684                }
685            }
686            if (skipDeleted || lifecycleWhereClauseQualifiers.contains(qual)) {
687                // add lifecycle state column
688                ModelProperty propertyInfo = model.getPropertyInfo(Model.MISC_LIFECYCLE_STATE_PROP);
689                Table table = getTable(database.getTable(propertyInfo.fragmentName), qual);
690                recordFragment(qual, table);
691            }
692            if (mixinTypeWhereClauseQualifiers.contains(qual)) {
693                recordFragment(qual, getTable(hierTable, qual));
694            }
695        }
696
697        // additional system columns to select on
698        if (!distinct) {
699            for (CmisSelector col : addedSystemColumns) {
700                recordSelectSelector(col);
701            }
702        } else {
703            if (!addedSystemColumns.isEmpty()) {
704                if (!virtualColumnNames.isEmpty()) {
705                    throw new QueryParseException(
706                            "Cannot use DISTINCT with virtual columns: " + StringUtils.join(virtualColumnNames, ", "));
707                }
708                if (addSystemColumns) {
709                    throw new QueryParseException("Cannot use DISTINCT without explicit " + PropertyIds.OBJECT_ID);
710                }
711                // don't add system columns as it would prevent DISTINCT from
712                // working
713            }
714        }
715
716        // for all qualifiers
717        for (String qual : allQualifiers) {
718            // include hier in fragments
719            recordFragment(qual, getTable(hierTable, qual));
720            // if only latest version include the version table
721            boolean versionable = versionableQualifiers.contains(qual);
722            if (searchLatestVersion && versionable) {
723                Table ver = database.getTable(Model.VERSION_TABLE_NAME);
724                recordFragment(qual, getTable(ver, qual));
725            }
726        }
727
728    }
729
730    /**
731     * Records a SELECT selector, and associates it to a database column.
732     */
733    protected void recordSelectSelector(CmisSelector sel) {
734        if (sel instanceof FunctionReference) {
735            FunctionReference fr = (FunctionReference) sel;
736            if (fr.getFunction() != CmisQlFunction.SCORE) {
737                throw new CmisRuntimeException("Unknown function: " + fr.getFunction());
738            }
739            String key = fr.getAliasName();
740            if (key == null) {
741                key = "SEARCH_SCORE"; // default, from spec
742            }
743            String scoreExprSql = fulltextMatchInfo.scoreExpr + " AS " + fulltextMatchInfo.scoreAlias;
744            SqlColumn c = new SqlColumn(scoreExprSql, fulltextMatchInfo.scoreCol, key);
745            realColumns.add(c);
746            if (fulltextMatchInfo.scoreExprParam != null) {
747                realColumnsParams.add(fulltextMatchInfo.scoreExprParam);
748            }
749            if (typeInfo != null) {
750                PropertyDecimalDefinitionImpl pd = new PropertyDecimalDefinitionImpl();
751                pd.setId(key);
752                pd.setQueryName(key);
753                pd.setCardinality(Cardinality.SINGLE);
754                pd.setDisplayName("Score");
755                pd.setLocalName("score");
756                typeInfo.put(key, pd);
757            }
758        } else { // sel instanceof ColumnReference
759            ColumnReference col = (ColumnReference) sel;
760            String qual = canonicalQualifier.get(col.getQualifier());
761
762            if (col.getPropertyQueryName().equals("*")) {
763                TypeDefinition type = getTypeForQualifier(qual);
764                for (PropertyDefinition<?> pd : type.getPropertyDefinitions().values()) {
765                    String id = pd.getId();
766                    if ((pd.getCardinality() == Cardinality.SINGLE //
767                            && Boolean.TRUE.equals(pd.isQueryable())) || id.equals(PropertyIds.BASE_TYPE_ID)) {
768                        ColumnReference c = new ColumnReference(qual, id);
769                        c.setTypeDefinition(id, type);
770                        recordSelectSelector(c);
771                    }
772                }
773                return;
774            }
775
776            String key = getColumnKey(col);
777            PropertyDefinition<?> pd = col.getPropertyDefinition();
778            Column column = getColumn(col);
779            if (column != null && pd.getCardinality() == Cardinality.SINGLE) {
780                col.setInfo(column);
781                recordColumnFragment(qual, column);
782                String sql = column.getFullQuotedName();
783                SqlColumn c = new SqlColumn(sql, column, key);
784                realColumns.add(c);
785            } else {
786                virtualColumns.put(key, col);
787                virtualColumnNames.add(key);
788                allQualifiers.add(qual);
789            }
790            if (typeInfo != null) {
791                typeInfo.put(key, pd);
792            }
793        }
794    }
795
796    protected static final String JOIN = "JOIN";
797
798    protected static final String WHERE = "WHERE";
799
800    protected static final String ORDER_BY = "ORDER BY";
801
802    /**
803     * Records a JOIN / WHERE / ORDER BY selector, and associates it to a database column.
804     */
805    protected void recordSelector(CmisSelector sel, String clauseType) {
806        if (sel instanceof FunctionReference) {
807            FunctionReference fr = (FunctionReference) sel;
808            if (clauseType != ORDER_BY) { // == ok
809                throw new QueryParseException("Cannot use function in " + clauseType + " clause: " + fr.getFunction());
810            }
811            // ORDER BY SCORE, nothing further to record
812            if (fulltextMatchInfo == null) {
813                throw new QueryParseException("Cannot use ORDER BY SCORE without CONTAINS");
814            }
815            return;
816        }
817        ColumnReference col = (ColumnReference) sel;
818        PropertyDefinition<?> pd = col.getPropertyDefinition();
819        boolean multi = pd.getCardinality() == Cardinality.MULTI;
820
821        // fetch column and associate it to the selector
822        Column column = getColumn(col);
823        if (!isFacetsColumn(col.getPropertyId()) && column == null) {
824            throw new QueryParseException(
825                    "Cannot use column in " + clauseType + " clause: " + col.getPropertyQueryName());
826        }
827        col.setInfo(column);
828        String qual = canonicalQualifier.get(col.getQualifier());
829
830        if (clauseType == WHERE && NuxeoTypeHelper.NX_LIFECYCLE_STATE.equals(col.getPropertyId())) {
831            // explicit lifecycle query: do not include the 'deleted' lifecycle
832            // filter
833            skipDeleted = false;
834            lifecycleWhereClauseQualifiers.add(qual);
835        }
836        if (clauseType == WHERE && isFacetsColumn(col.getPropertyId())) {
837            mixinTypeWhereClauseQualifiers.add(qual);
838        }
839        // record as a needed fragment
840        if (!multi) {
841            recordColumnFragment(qual, column);
842        }
843    }
844
845    /**
846     * Records a database column's fragment (to know what to join).
847     */
848    protected void recordColumnFragment(String qual, Column column) {
849        recordFragment(qual, column.getTable());
850    }
851
852    /**
853     * Records a database table and qualifier (to know what to join).
854     */
855    protected void recordFragment(String qual, Table table) {
856        String fragment = table.getKey();
857        Map<String, Table> tablesByFragment = allTables.get(qual);
858        if (tablesByFragment == null) {
859            allTables.put(qual, tablesByFragment = new HashMap<>());
860        }
861        tablesByFragment.put(fragment, table);
862        allQualifiers.add(qual);
863    }
864
865    /**
866     * Finds what qualifiers are allowed and to what correlation name they are mapped.
867     */
868    protected void resolveQualifiers() {
869        Map<String, String> types = query.getTypes();
870        Map<String, AtomicInteger> typeCount = new HashMap<>();
871        for (Entry<String, String> en : types.entrySet()) {
872            String qual = en.getKey();
873            String typeQueryName = en.getValue();
874            qualifierToType.put(qual, typeQueryName);
875            // if an alias, use as its own correlation name
876            canonicalQualifier.put(qual, qual);
877            // also use alias as correlation name for this type
878            // (ambiguous types removed later)
879            canonicalQualifier.put(typeQueryName, qual);
880            // count type use
881            if (!typeCount.containsKey(typeQueryName)) {
882                typeCount.put(typeQueryName, new AtomicInteger(0));
883            }
884            typeCount.get(typeQueryName).incrementAndGet();
885        }
886        for (Entry<String, AtomicInteger> en : typeCount.entrySet()) {
887            String typeQueryName = en.getKey();
888            if (en.getValue().get() == 1) {
889                // for types used once, allow direct type reference
890                qualifierToType.put(typeQueryName, typeQueryName);
891            } else {
892                // ambiguous type, not legal as qualifier
893                canonicalQualifier.remove(typeQueryName);
894            }
895        }
896        // if only one type, allow omitted qualifier (null)
897        if (types.size() == 1) {
898            String typeQueryName = types.values().iterator().next();
899            qualifierToType.put(null, typeQueryName);
900            // correlation name is actually null for all qualifiers
901            for (String qual : qualifierToType.keySet()) {
902                canonicalQualifier.put(qual, null);
903            }
904        }
905    }
906
907    /**
908     * Finds a database column from a CMIS reference.
909     */
910    protected Column getColumn(ColumnReference col) {
911        String qual = canonicalQualifier.get(col.getQualifier());
912        String id = col.getPropertyId();
913        Column column;
914        if (id.startsWith(CMIS_PREFIX) || id.startsWith(NX_PREFIX)) {
915            column = getSystemColumn(qual, id);
916        } else {
917            ModelProperty propertyInfo = model.getPropertyInfo(id);
918            boolean multi = propertyInfo.propertyType.isArray();
919            Table table = database.getTable(propertyInfo.fragmentName);
920            String key = multi ? Model.COLL_TABLE_VALUE_KEY : propertyInfo.fragmentKey;
921            column = getTable(table, qual).getColumn(key);
922        }
923        return column;
924    }
925
926    protected Column getSystemColumn(String qual, String id) {
927        Column column = getSystemColumn(id);
928        if (column != null && qual != null) {
929            // alias table according to qualifier
930            Table table = column.getTable();
931            column = getTable(table, qual).getColumn(column.getKey());
932            // TODO ensure key == name, or add getName()
933        }
934        return column;
935    }
936
937    protected Column getSystemColumn(String id) {
938        if (id.equals(PropertyIds.OBJECT_ID)) {
939            return hierTable.getColumn(Model.MAIN_KEY);
940        }
941        if (id.equals(PropertyIds.PARENT_ID)) {
942            return hierTable.getColumn(Model.HIER_PARENT_KEY);
943        }
944        if (id.equals(NuxeoTypeHelper.NX_PARENT_ID)) {
945            return hierTable.getColumn(Model.HIER_PARENT_KEY);
946        }
947        if (id.equals(NuxeoTypeHelper.NX_PATH_SEGMENT)) {
948            return hierTable.getColumn(Model.HIER_CHILD_NAME_KEY);
949        }
950        if (id.equals(NuxeoTypeHelper.NX_POS)) {
951            return hierTable.getColumn(Model.HIER_CHILD_POS_KEY);
952        }
953        if (id.equals(PropertyIds.OBJECT_TYPE_ID)) {
954            // joinedHierTable
955            return hierTable.getColumn(Model.MAIN_PRIMARY_TYPE_KEY);
956        }
957        if (id.equals(PropertyIds.VERSION_LABEL)) {
958            return database.getTable(Model.VERSION_TABLE_NAME).getColumn(Model.VERSION_LABEL_KEY);
959        }
960        if (id.equals(PropertyIds.IS_LATEST_MAJOR_VERSION)) {
961            return database.getTable(Model.VERSION_TABLE_NAME).getColumn(Model.VERSION_IS_LATEST_MAJOR_KEY);
962        }
963        if (id.equals(PropertyIds.IS_LATEST_VERSION)) {
964            return database.getTable(Model.VERSION_TABLE_NAME).getColumn(Model.VERSION_IS_LATEST_KEY);
965        }
966        if (id.equals(NuxeoTypeHelper.NX_ISVERSION)) {
967            return database.getTable(Model.HIER_TABLE_NAME).getColumn(Model.MAIN_IS_VERSION_KEY);
968        }
969        if (id.equals(NuxeoTypeHelper.NX_ISCHECKEDIN)) {
970            return database.getTable(Model.HIER_TABLE_NAME).getColumn(Model.MAIN_CHECKED_IN_KEY);
971        }
972        if (id.equals(NuxeoTypeHelper.NX_LIFECYCLE_STATE)) {
973            ModelProperty propertyInfo = model.getPropertyInfo(Model.MISC_LIFECYCLE_STATE_PROP);
974            return database.getTable(propertyInfo.fragmentName).getColumn(propertyInfo.fragmentKey);
975        }
976        if (id.equals(PropertyIds.NAME)) {
977            return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_TITLE_KEY);
978        }
979        if (id.equals(PropertyIds.DESCRIPTION)) {
980            return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_DESCRIPTION_KEY);
981        }
982        if (id.equals(PropertyIds.CREATED_BY)) {
983            return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_CREATOR_KEY);
984        }
985        if (id.equals(PropertyIds.CREATION_DATE)) {
986            return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_CREATED_KEY);
987        }
988        if (id.equals(PropertyIds.LAST_MODIFICATION_DATE)) {
989            return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_MODIFIED_KEY);
990        }
991        if (id.equals(PropertyIds.LAST_MODIFIED_BY)) {
992            return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_LAST_CONTRIBUTOR_KEY);
993        }
994        if (id.equals(PropertyIds.SOURCE_ID)) {
995            return database.getTable(REL_FRAGMENT_NAME).getColumn(REL_SOURCE_KEY);
996        }
997        if (id.equals(PropertyIds.TARGET_ID)) {
998            return database.getTable(REL_FRAGMENT_NAME).getColumn(REL_TARGET_KEY);
999        }
1000        return null;
1001    }
1002
1003    /** Get key to use in data returned to high-level caller. */
1004    protected static String getColumnKey(ColumnReference col) {
1005        String alias = col.getAliasName();
1006        if (alias != null) {
1007            return alias;
1008        }
1009        return getPropertyKey(col.getQualifier(), col.getPropertyQueryName());
1010    }
1011
1012    protected static String getPropertyKey(String qual, String id) {
1013        if (qual == null) {
1014            return id;
1015        }
1016        return qual + '.' + id;
1017    }
1018
1019    protected TypeDefinition getTypeForQualifier(String qual) {
1020        String typeQueryName = qualifierToType.get(qual);
1021        return query.getTypeDefinitionFromQueryName(typeQueryName);
1022    }
1023
1024    protected Table getTable(Table table, String qual) {
1025        if (qual == null) {
1026            return table;
1027        } else {
1028            return new TableAlias(table, getTableAlias(table, qual));
1029        }
1030    }
1031
1032    protected String getTableAlias(Table table, String qual) {
1033        return "_" + qual + "_" + table.getPhysicalName();
1034    }
1035
1036    /**
1037     * Map maker that can deal with aliased column names and computed values.
1038     */
1039    // static to avoid keeping the whole QueryMaker in the returned object
1040    public static class CMISQLMapMaker implements MapMaker {
1041
1042        protected List<SqlColumn> realColumns;
1043
1044        protected Map<String, ColumnReference> virtualColumns;
1045
1046        protected NuxeoCmisService service;
1047
1048        public CMISQLMapMaker(List<SqlColumn> realColumns, Map<String, ColumnReference> virtualColumns,
1049                NuxeoCmisService service) {
1050            this.realColumns = realColumns;
1051            this.virtualColumns = virtualColumns;
1052            this.service = service;
1053        }
1054
1055        @Override
1056        public Map<String, Serializable> makeMap(ResultSet rs) throws SQLException {
1057            Map<String, Serializable> map = new HashMap<>();
1058
1059            // get values from result set
1060            int i = 1;
1061            for (SqlColumn rc : realColumns) {
1062                Serializable value = rc.column.getFromResultSet(rs, i++);
1063                String key = rc.column.getKey();
1064                // type conversion to CMIS values
1065                if (value instanceof Long) {
1066                    value = BigInteger.valueOf(((Long) value).longValue());
1067                } else if (value instanceof Integer) {
1068                    value = BigInteger.valueOf(((Integer) value).intValue());
1069                } else if (value instanceof Double) {
1070                    value = BigDecimal.valueOf(((Double) value).doubleValue());
1071                } else if (value == null) {
1072                    // special handling of some columns where NULL means FALSE
1073                    String column = rc.column.getTable().getRealTable().getKey() + " " + key;
1074                    if (NULL_IS_FALSE_COLUMNS.contains(column)) {
1075                        value = Boolean.FALSE;
1076                    }
1077                }
1078                if (Model.MAIN_KEY.equals(key) || Model.HIER_PARENT_KEY.equals(key)) {
1079                    value = String.valueOf(value); // idToString
1080                }
1081                map.put(rc.key, value);
1082            }
1083
1084            // virtual values
1085            // map to store actual data for each qualifier
1086            TypeManagerImpl typeManager = service.getTypeManager();
1087            Map<String, NuxeoObjectData> datas = null;
1088            for (Entry<String, ColumnReference> vc : virtualColumns.entrySet()) {
1089                String key = vc.getKey();
1090                ColumnReference col = vc.getValue();
1091                String qual = col.getQualifier();
1092                if (col.getPropertyId().equals(PropertyIds.BASE_TYPE_ID)) {
1093                    // special case, no need to get full Nuxeo Document
1094                    String typeId = (String) map.get(getPropertyKey(qual, PropertyIds.OBJECT_TYPE_ID));
1095                    TypeDefinitionContainer type = typeManager.getTypeById(typeId);
1096                    String baseTypeId = type.getTypeDefinition().getBaseTypeId().value();
1097                    map.put(key, baseTypeId);
1098                    continue;
1099                }
1100                if (datas == null) {
1101                    datas = new HashMap<>(2);
1102                }
1103                NuxeoObjectData data = datas.get(qual);
1104                if (data == null) {
1105                    // find main id for this qualifier in the result set
1106                    // (main id always included in joins)
1107                    // TODO check what happens if cmis:objectId is aliased
1108                    String id = (String) map.get(getPropertyKey(qual, PropertyIds.OBJECT_ID));
1109                    try {
1110                        // reentrant call to the same session, but the MapMaker
1111                        // is only called from the IterableQueryResult in
1112                        // queryAndFetch which manipulates no session state
1113                        // TODO constructing the DocumentModel (in
1114                        // NuxeoObjectData) is expensive, try to get value
1115                        // directly
1116                        data = (NuxeoObjectData) service.getObject(service.getNuxeoRepository().getId(), id, null, null,
1117                                null, null, null, null, null);
1118                    } catch (CmisRuntimeException e) {
1119                        log.error("Cannot get document: " + id, e);
1120                    }
1121                    datas.put(qual, data);
1122                }
1123                Serializable v;
1124                if (data == null) {
1125                    // could not fetch
1126                    v = null;
1127                } else {
1128                    NuxeoPropertyDataBase<?> pd = (NuxeoPropertyDataBase<?>) data.getProperty(col.getPropertyId());
1129                    if (pd == null) {
1130                        v = null;
1131                    } else {
1132                        if (pd.getPropertyDefinition().getCardinality() == Cardinality.SINGLE) {
1133                            v = (Serializable) pd.getFirstValue();
1134                        } else {
1135                            v = (Serializable) pd.getValues();
1136                        }
1137                    }
1138                }
1139                map.put(key, v);
1140            }
1141
1142            return map;
1143        }
1144    }
1145
1146    /**
1147     * Walker of the WHERE clause to gather fulltext info.
1148     */
1149    public class AnalyzingWalker extends AbstractPredicateWalker {
1150
1151        public static final String NX_FULLTEXT_INDEX_PREFIX = "nx:";
1152
1153        public boolean hasContains;
1154
1155        @Override
1156        public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
1157            if (hasContains && Framework.getService(ConfigurationService.class)
1158                                        .isBooleanPropertyFalse(NuxeoRepository.RELAX_CMIS_SPEC)) {
1159                throw new QueryParseException("At most one CONTAINS() is allowed");
1160            }
1161            hasContains = true;
1162
1163            String qual = qualNode == null ? null : qualNode.getText();
1164            qual = canonicalQualifier.get(qual);
1165            Column column = getSystemColumn(qual, PropertyIds.OBJECT_ID);
1166            String statement = (String) super.walkString(queryNode);
1167            String indexName = Model.FULLTEXT_DEFAULT_INDEX;
1168
1169            // micro parsing of the fulltext statement to perform fulltext
1170            // search on a non default index
1171            if (statement.startsWith(NX_FULLTEXT_INDEX_PREFIX)) {
1172                statement = statement.substring(NX_FULLTEXT_INDEX_PREFIX.length());
1173                int firstColumnIdx = statement.indexOf(':');
1174                if (firstColumnIdx > 0 && firstColumnIdx < statement.length() - 1) {
1175                    String requestedIndexName = statement.substring(0, firstColumnIdx);
1176                    statement = statement.substring(firstColumnIdx + 1);
1177                    if (model.getFulltextConfiguration().indexNames.contains(requestedIndexName)) {
1178                        indexName = requestedIndexName;
1179                    } else {
1180                        throw new QueryParseException("No such fulltext index: " + requestedIndexName);
1181                    }
1182                } else {
1183                    log.warn(String.format("fail to microparse custom fulltext index:" + " fallback to '%s'",
1184                            indexName));
1185                }
1186            }
1187            // CMIS syntax to our internal google-like internal syntax
1188            statement = cmisToFulltextQuery(statement);
1189            // internal syntax to backend syntax
1190            statement = dialect.getDialectFulltextQuery(statement);
1191            fulltextMatchInfo = dialect.getFulltextScoredMatchInfo(statement, indexName, 1, column, model, database);
1192            return null;
1193        }
1194    }
1195
1196    protected static String cmisToFulltextQuery(String statement) {
1197        // internal syntax has implicit AND
1198        statement = statement.replace(" and ", " ");
1199        statement = statement.replace(" AND ", " ");
1200        return statement;
1201    }
1202
1203    /**
1204     * Walker of the WHERE clause that generates final SQL.
1205     */
1206    public class GeneratingWalker extends AbstractPredicateWalker {
1207
1208        public StringBuilder whereBuf = new StringBuilder();
1209
1210        public LinkedList<Serializable> whereBufParams = new LinkedList<Serializable>();
1211
1212        /** joins added by fulltext match */
1213        public final List<org.nuxeo.ecm.core.storage.sql.jdbc.db.Join> ftJoins = new LinkedList<org.nuxeo.ecm.core.storage.sql.jdbc.db.Join>();
1214
1215        @Override
1216        public Boolean walkNot(Tree opNode, Tree node) {
1217            whereBuf.append("NOT ");
1218            walkPredicate(node);
1219            return null;
1220        }
1221
1222        @Override
1223        public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) {
1224            whereBuf.append("(");
1225            walkPredicate(leftNode);
1226            whereBuf.append(" AND ");
1227            walkPredicate(rightNode);
1228            whereBuf.append(")");
1229            return null;
1230        }
1231
1232        @Override
1233        public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) {
1234            whereBuf.append("(");
1235            walkPredicate(leftNode);
1236            whereBuf.append(" OR ");
1237            walkPredicate(rightNode);
1238            whereBuf.append(")");
1239            return null;
1240        }
1241
1242        @Override
1243        public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) {
1244            if (isFacetsColumn(leftNode.getText())) {
1245                walkFacets(opNode, leftNode, rightNode);
1246                return null;
1247            }
1248            if (leftNode.getType() == CmisQlStrictLexer.COL && rightNode.getType() == CmisQlStrictLexer.BOOL_LIT
1249                    && !Boolean.parseBoolean(rightNode.getText())) {
1250                // special handling of the " = false" case for column where
1251                // NULL means false
1252                walkIsNullOrFalse(leftNode);
1253                return null;
1254            }
1255            // normal case
1256            walkExpr(leftNode);
1257            whereBuf.append(" = ");
1258            walkExpr(rightNode);
1259            return null;
1260        }
1261
1262        @Override
1263        public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) {
1264            if (leftNode.getType() == CmisQlStrictLexer.COL && rightNode.getType() == CmisQlStrictLexer.BOOL_LIT
1265                    && Boolean.parseBoolean(rightNode.getText())) {
1266                // special handling of the " <> true" case for column where
1267                // NULL means false
1268                walkIsNullOrFalse(leftNode);
1269                return null;
1270            }
1271            walkExpr(leftNode);
1272            whereBuf.append(" <> ");
1273            walkExpr(rightNode);
1274            return null;
1275        }
1276
1277        protected void walkIsNullOrFalse(Tree leftNode) {
1278            Column c = resolveColumn(leftNode);
1279            String columnSpec = c.getTable().getRealTable().getKey() + " " + c.getKey();
1280            if (NULL_IS_FALSE_COLUMNS.contains(columnSpec)) {
1281                // treat NULL and FALSE as equivalent
1282                whereBuf.append("(");
1283                walkExpr(leftNode);
1284                whereBuf.append(" IS NULL OR ");
1285                walkExpr(leftNode);
1286                whereBuf.append(" = ?)");
1287                whereBufParams.add(Boolean.FALSE);
1288            } else {
1289                // explicit false equality test
1290                walkExpr(leftNode);
1291                whereBuf.append(" = ?");
1292                whereBufParams.add(Boolean.FALSE);
1293            }
1294        }
1295
1296        @Override
1297        public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) {
1298            walkExpr(leftNode);
1299            whereBuf.append(" > ");
1300            walkExpr(rightNode);
1301            return null;
1302        }
1303
1304        @Override
1305        public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
1306            walkExpr(leftNode);
1307            whereBuf.append(" >= ");
1308            walkExpr(rightNode);
1309            return null;
1310        }
1311
1312        @Override
1313        public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) {
1314            walkExpr(leftNode);
1315            whereBuf.append(" < ");
1316            walkExpr(rightNode);
1317            return null;
1318        }
1319
1320        @Override
1321        public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
1322            walkExpr(leftNode);
1323            whereBuf.append(" <= ");
1324            walkExpr(rightNode);
1325            return null;
1326        }
1327
1328        @Override
1329        public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) {
1330            walkExpr(colNode);
1331            whereBuf.append(" IN ");
1332            walkExpr(listNode);
1333            return null;
1334        }
1335
1336        @Override
1337        public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) {
1338            walkExpr(colNode);
1339            whereBuf.append(" NOT IN ");
1340            walkExpr(listNode);
1341            return null;
1342        }
1343
1344        @Override
1345        public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) {
1346            if (isFacetsColumn(resolveColumnReference(colNode).getName())) {
1347                walkFacets(opNode, colNode, listNode);
1348                return null;
1349            }
1350            walkAny(colNode, "IN", listNode);
1351            return null;
1352        }
1353
1354        @Override
1355        public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) {
1356            if (isFacetsColumn(resolveColumnReference(colNode).getName())) {
1357                walkFacets(opNode, colNode, listNode);
1358                return null;
1359            }
1360            walkAny(colNode, "NOT IN", listNode);
1361            return null;
1362        }
1363
1364        @Override
1365        public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) {
1366            if (isFacetsColumn(resolveColumnReference(colNode).getName())) {
1367                walkFacets(opNode, colNode, literalNode);
1368                return null;
1369            }
1370            // note that argument order is reversed
1371            walkAny(colNode, "=", literalNode);
1372            return null;
1373        }
1374
1375        protected void walkAny(Tree colNode, String op, Tree exprNode) {
1376            int token = ((Tree) colNode).getTokenStartIndex();
1377            ColumnReference col = (ColumnReference) query.getColumnReference(Integer.valueOf(token));
1378            PropertyDefinition<?> pd = col.getPropertyDefinition();
1379            if (pd.getCardinality() != Cardinality.MULTI) {
1380                throw new QueryParseException(
1381                        "Cannot use " + op + " ANY with single-valued property: " + col.getPropertyQueryName());
1382            }
1383            Column column = (Column) col.getInfo();
1384            String qual = canonicalQualifier.get(col.getQualifier());
1385            // we need the real table and column in the subquery
1386            Table realTable = column.getTable().getRealTable();
1387            Column realColumn = realTable.getColumn(column.getKey());
1388            Column hierMainColumn = getTable(hierTable, qual).getColumn(Model.MAIN_KEY);
1389            Column multiMainColumn = realTable.getColumn(Model.MAIN_KEY);
1390
1391            whereBuf.append("EXISTS (SELECT 1 FROM ");
1392            whereBuf.append(realTable.getQuotedName());
1393            whereBuf.append(" WHERE ");
1394            whereBuf.append(hierMainColumn.getFullQuotedName());
1395            whereBuf.append(" = ");
1396            whereBuf.append(multiMainColumn.getFullQuotedName());
1397            whereBuf.append(" AND ");
1398            whereBuf.append(realColumn.getFullQuotedName());
1399            whereBuf.append(" ");
1400            whereBuf.append(op);
1401            whereBuf.append(" ");
1402            walkExpr(exprNode);
1403            whereBuf.append(")");
1404        }
1405
1406        @Override
1407        public Boolean walkIsNull(Tree opNode, Tree colNode) {
1408            return walkIsNullOrIsNotNull(colNode, true);
1409        }
1410
1411        @Override
1412        public Boolean walkIsNotNull(Tree opNode, Tree colNode) {
1413            return walkIsNullOrIsNotNull(colNode, false);
1414        }
1415
1416        protected Boolean walkIsNullOrIsNotNull(Tree colNode, boolean isNull) {
1417            int token = ((Tree) colNode).getTokenStartIndex();
1418            ColumnReference col = (ColumnReference) query.getColumnReference(Integer.valueOf(token));
1419            PropertyDefinition<?> pd = col.getPropertyDefinition();
1420            boolean multi = pd.getCardinality() == Cardinality.MULTI;
1421            if (multi) {
1422                // we need the real table and column in the subquery
1423                Column column = (Column) col.getInfo();
1424                String qual = canonicalQualifier.get(col.getQualifier());
1425                Table realTable = column.getTable().getRealTable();
1426                Column hierMainColumn = getTable(hierTable, qual).getColumn(Model.MAIN_KEY);
1427                Column multiMainColumn = realTable.getColumn(Model.MAIN_KEY);
1428                if (isNull) {
1429                    whereBuf.append("NOT ");
1430                }
1431                whereBuf.append("EXISTS (SELECT 1 FROM ");
1432                whereBuf.append(realTable.getQuotedName());
1433                whereBuf.append(" WHERE ");
1434                whereBuf.append(hierMainColumn.getFullQuotedName());
1435                whereBuf.append(" = ");
1436                whereBuf.append(multiMainColumn.getFullQuotedName());
1437                whereBuf.append(')');
1438            } else {
1439                walkExpr(colNode);
1440                whereBuf.append(isNull ? " IS NULL" : " IS NOT NULL");
1441            }
1442            return null;
1443        }
1444
1445        @Override
1446        public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) {
1447            walkExpr(colNode);
1448            whereBuf.append(" LIKE ");
1449            walkExpr(stringNode);
1450            return null;
1451        }
1452
1453        @Override
1454        public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) {
1455            walkExpr(colNode);
1456            whereBuf.append(" NOT LIKE ");
1457            walkExpr(stringNode);
1458            return null;
1459        }
1460
1461        @Override
1462        public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
1463            if (fulltextMatchInfo.joins != null) {
1464                ftJoins.addAll(fulltextMatchInfo.joins);
1465            }
1466            whereBuf.append(fulltextMatchInfo.whereExpr);
1467            if (fulltextMatchInfo.whereExprParam != null) {
1468                whereBufParams.add(fulltextMatchInfo.whereExprParam);
1469            }
1470            return null;
1471        }
1472
1473        @Override
1474        public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) {
1475            String qual = qualNode == null ? null : qualNode.getText();
1476            qual = canonicalQualifier.get(qual);
1477            // this is from the hierarchy table which is always present
1478            Column column = getSystemColumn(qual, PropertyIds.PARENT_ID);
1479            whereBuf.append(column.getFullQuotedName());
1480            whereBuf.append(" = ?");
1481            String id = (String) super.walkString(paramNode);
1482            whereBufParams.add(model.idFromString(id));
1483            return null;
1484        }
1485
1486        @Override
1487        public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) {
1488            String qual = qualNode == null ? null : qualNode.getText();
1489            qual = canonicalQualifier.get(qual);
1490            // this is from the hierarchy table which is always present
1491            Column column = getSystemColumn(qual, PropertyIds.OBJECT_ID);
1492            String id = (String) super.walkString(paramNode);
1493            String sql = dialect.getInTreeSql(column.getFullQuotedName(), id);
1494            if (sql == null) {
1495                whereBuf.append("0=1");
1496            } else {
1497                whereBuf.append(sql);
1498                whereBufParams.add(model.idFromString(id));
1499            }
1500            return null;
1501        }
1502
1503        @Override
1504        public Object walkList(Tree node) {
1505            whereBuf.append("(");
1506            for (int i = 0; i < node.getChildCount(); i++) {
1507                if (i != 0) {
1508                    whereBuf.append(", ");
1509                }
1510                Tree child = node.getChild(i);
1511                walkExpr(child);
1512            }
1513            whereBuf.append(")");
1514            return null;
1515        }
1516
1517        @Override
1518        public Object walkBoolean(Tree node) {
1519            Serializable value = (Serializable) super.walkBoolean(node);
1520            whereBuf.append("?");
1521            whereBufParams.add(value);
1522            return null;
1523        }
1524
1525        @Override
1526        public Object walkNumber(Tree node) {
1527            Serializable value = (Serializable) super.walkNumber(node);
1528            whereBuf.append("?");
1529            whereBufParams.add(value);
1530            return null;
1531        }
1532
1533        @Override
1534        public Object walkString(Tree node) {
1535            Serializable value = (Serializable) super.walkString(node);
1536            whereBuf.append("?");
1537            whereBufParams.add(value);
1538            return null;
1539        }
1540
1541        @Override
1542        public Object walkTimestamp(Tree node) {
1543            Serializable value = (Serializable) super.walkTimestamp(node);
1544            whereBuf.append("?");
1545            whereBufParams.add(value);
1546            return null;
1547        }
1548
1549        @Override
1550        public Object walkCol(Tree node) {
1551            whereBuf.append(resolveColumn(node).getFullQuotedName());
1552            return null;
1553        }
1554
1555        public ColumnReference resolveColumnReference(Tree node) {
1556            int token = node.getTokenStartIndex();
1557            CmisSelector sel = query.getColumnReference(Integer.valueOf(token));
1558            if (sel instanceof ColumnReference) {
1559                return (ColumnReference) sel;
1560            } else {
1561                throw new QueryParseException("Cannot use column in WHERE clause: " + sel.getName());
1562            }
1563        }
1564
1565        public Column resolveColumn(Tree node) {
1566            return (Column) resolveColumnReference(node).getInfo();
1567        }
1568
1569        protected void walkFacets(Tree opNode, Tree colNodel, Tree literalNode) {
1570            boolean include;
1571            Set<String> mixins;
1572
1573            int opType = opNode.getType();
1574            if (opType == CmisQlStrictLexer.EQ_ANY) {
1575                include = true;
1576                if (literalNode.getType() != CmisQlStrictLexer.STRING_LIT) {
1577                    throw new QueryParseException(colNodel.getText() + " = requires literal string as right argument");
1578                }
1579                String value = super.walkString(literalNode).toString();
1580                mixins = Collections.singleton(value);
1581            } else if (opType == CmisQlStrictLexer.IN_ANY || opType == CmisQlStrictLexer.NOT_IN_ANY) {
1582                include = opType == CmisQlStrictLexer.IN_ANY;
1583                mixins = new TreeSet<>();
1584                for (int i = 0; i < literalNode.getChildCount(); i++) {
1585                    mixins.add(super.walkString(literalNode.getChild(i)).toString());
1586                }
1587            } else {
1588                throw new QueryParseException(colNodel.getText() + " unsupported operator: " + opNode.getText());
1589            }
1590
1591            /*
1592             * Primary types - static mixins
1593             */
1594            Set<String> types;
1595            if (include) {
1596                types = new HashSet<>();
1597                for (String mixin : mixins) {
1598                    types.addAll(model.getMixinDocumentTypes(mixin));
1599                }
1600            } else {
1601                types = new HashSet<>(model.getDocumentTypes());
1602                for (String mixin : mixins) {
1603                    types.removeAll(model.getMixinDocumentTypes(mixin));
1604                }
1605            }
1606
1607            /*
1608             * Instance mixins
1609             */
1610            Set<String> instanceMixins = new HashSet<>();
1611            for (String mixin : mixins) {
1612                if (!MIXINS_NOT_PER_INSTANCE.contains(mixin)) {
1613                    instanceMixins.add(mixin);
1614                }
1615            }
1616
1617            /*
1618             * SQL generation
1619             */
1620
1621            ColumnReference facetsCol = resolveColumnReference(colNodel);
1622            String qual = canonicalQualifier.get(facetsCol.getQualifier());
1623            Table table = getTable(hierTable, qual);
1624
1625            if (!types.isEmpty()) {
1626                Column col = table.getColumn(Model.MAIN_PRIMARY_TYPE_KEY);
1627                whereBuf.append(col.getFullQuotedName());
1628                whereBuf.append(" IN ");
1629                whereBuf.append('(');
1630                for (Iterator<String> it = types.iterator(); it.hasNext();) {
1631                    whereBuf.append('?');
1632                    whereBufParams.add(it.next());
1633                    if (it.hasNext()) {
1634                        whereBuf.append(", ");
1635                    }
1636                }
1637                whereBuf.append(')');
1638
1639                if (!instanceMixins.isEmpty()) {
1640                    whereBuf.append(include ? " OR " : " AND ");
1641                }
1642            }
1643
1644            if (!instanceMixins.isEmpty()) {
1645                whereBuf.append('(');
1646                Column mixinsColumn = table.getColumn(Model.MAIN_MIXIN_TYPES_KEY);
1647                String[] returnParam = new String[1];
1648                for (Iterator<String> it = instanceMixins.iterator(); it.hasNext();) {
1649                    String mixin = it.next();
1650                    String sql = dialect.getMatchMixinType(mixinsColumn, mixin, include, returnParam);
1651                    whereBuf.append(sql);
1652                    if (returnParam[0] != null) {
1653                        whereBufParams.add(returnParam[0]);
1654                    }
1655                    if (it.hasNext()) {
1656                        whereBuf.append(include ? " OR " : " AND ");
1657                    }
1658                }
1659                if (!include) {
1660                    whereBuf.append(" OR ");
1661                    whereBuf.append(mixinsColumn.getFullQuotedName());
1662                    whereBuf.append(" IS NULL");
1663                }
1664                whereBuf.append(')');
1665            }
1666
1667            if (types.isEmpty() && instanceMixins.isEmpty()) {
1668                whereBuf.append(include ? "0=1" : "0=0");
1669            }
1670        }
1671    }
1672}