001/*
002 * (C) Copyright 2006-2011 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 */
019
020package org.nuxeo.ecm.core.storage.sql.jdbc;
021
022import java.io.Serializable;
023import java.sql.Types;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.LinkedList;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.regex.Pattern;
035
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.nuxeo.common.utils.FullTextUtils;
040import org.nuxeo.ecm.core.api.impl.FacetFilter;
041import org.nuxeo.ecm.core.query.QueryFilter;
042import org.nuxeo.ecm.core.query.QueryParseException;
043import org.nuxeo.ecm.core.query.sql.NXQL;
044import org.nuxeo.ecm.core.query.sql.SQLQueryParser;
045import org.nuxeo.ecm.core.query.sql.model.BooleanLiteral;
046import org.nuxeo.ecm.core.query.sql.model.DateLiteral;
047import org.nuxeo.ecm.core.query.sql.model.DefaultQueryVisitor;
048import org.nuxeo.ecm.core.query.sql.model.DoubleLiteral;
049import org.nuxeo.ecm.core.query.sql.model.Expression;
050import org.nuxeo.ecm.core.query.sql.model.FromClause;
051import org.nuxeo.ecm.core.query.sql.model.FromList;
052import org.nuxeo.ecm.core.query.sql.model.Function;
053import org.nuxeo.ecm.core.query.sql.model.IntegerLiteral;
054import org.nuxeo.ecm.core.query.sql.model.Literal;
055import org.nuxeo.ecm.core.query.sql.model.LiteralList;
056import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
057import org.nuxeo.ecm.core.query.sql.model.Operand;
058import org.nuxeo.ecm.core.query.sql.model.Operator;
059import org.nuxeo.ecm.core.query.sql.model.OrderByClause;
060import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
061import org.nuxeo.ecm.core.query.sql.model.OrderByList;
062import org.nuxeo.ecm.core.query.sql.model.Predicate;
063import org.nuxeo.ecm.core.query.sql.model.Reference;
064import org.nuxeo.ecm.core.query.sql.model.SQLQuery;
065import org.nuxeo.ecm.core.query.sql.model.SelectClause;
066import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
067import org.nuxeo.ecm.core.query.sql.model.WhereClause;
068import org.nuxeo.ecm.core.storage.sql.ColumnType;
069import org.nuxeo.ecm.core.storage.sql.ColumnType.WrappedId;
070import org.nuxeo.ecm.core.storage.sql.Model;
071import org.nuxeo.ecm.core.storage.sql.ModelProperty;
072import org.nuxeo.ecm.core.storage.sql.Session.PathResolver;
073import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.ColumnMapMaker;
074import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.SQLInfoSelect;
075import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
076import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database;
077import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join;
078import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select;
079import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
080import org.nuxeo.ecm.core.storage.sql.jdbc.db.TableAlias;
081import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
082import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect.ArraySubQuery;
083import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect.FulltextMatchInfo;
084
085/**
086 * Transformer of NXQL queries into underlying SQL queries to the actual database.
087 * <p>
088 * The examples below are using the NXQL statement syntax:
089 *
090 * <pre>
091 * SELECT * FROM File
092 * WHERE
093 *   dc:title = 'abc'
094 *   AND uid:uid = '123'
095 *   AND dc:contributors = 'bob'    -- multi-valued
096 * </pre>
097 *
098 * If there are no proxies (ecm:isProxy = 0) we get:
099 *
100 * <pre>
101 * SELECT hierarchy.id
102 *   FROM hierarchy
103 *   LEFT JOIN dublincore ON hierarchy.id = dublincore.id
104 *   LEFT JOIN uid ON hierarchy.id = uid.id
105 * WHERE
106 *   hierarchy.primarytype IN ('File', 'SubFile')
107 *   AND dublincore.title = 'abc'
108 *   AND uid.uid = '123'
109 *   AND EXISTS (SELECT 1 FROM dc_contributors WHERE hierarchy.id = dc_contributors.id
110 *               AND dc_contributors.item = 'bob')
111 *   AND NX_ACCESS_ALLOWED(hierarchy.id, 'user1|user2', 'perm1|perm2')
112 * </pre>
113 *
114 * The data tables (dublincore, uid) are joined using a LEFT JOIN, as the schema may not be present on all documents but
115 * this shouldn't prevent the WHERE clause from being evaluated. Complex properties are matched using an EXISTS and a
116 * subselect. When proxies are matched (ecm:isProxy = 1) there are two additional FULL JOINs. Security checks, id, name,
117 * parents and path use the base hierarchy (_H), but all other data use the joined hierarchy.
118 *
119 * <pre>
120 * SELECT _H.id
121 *   FROM hierarchy _H
122 *   JOIN proxies ON _H.id = proxies.id                     -- proxy full join
123 *   JOIN hierarchy ON hierarchy.id = proxies.targetid      -- proxy full join
124 *   LEFT JOIN dublincore ON hierarchy.id = dublincore.id
125 *   LEFT JOIN uid ON hierarchy.id = uid.id
126 * WHERE
127 *   hierarchy.primarytype IN ('File', 'SubFile')
128 *   AND dublincore.title = 'abc'
129 *   AND uid.uid = '123'
130 *   AND EXISTS (SELECT 1 FROM dc_contributors WHERE hierarchy.id = dc_contributors.id
131 *               AND dc_contributors.item = 'bob')
132 *   AND NX_ACCESS_ALLOWED(_H.id, 'user1|user2', 'perm1|perm2') -- uses _H
133 * </pre>
134 *
135 * When both normal documents and proxies are matched, we UNION ALL the two queries. If an ORDER BY is requested, then
136 * columns from the inner SELECTs have to be aliased so that an outer ORDER BY can user their names.
137 *
138 * @author Florent Guillaume
139 */
140public class NXQLQueryMaker implements QueryMaker {
141
142    private static final Log log = LogFactory.getLog(NXQLQueryMaker.class);
143
144    public static final String TYPE_DOCUMENT = "Document";
145
146    public static final String TYPE_RELATION = "Relation";
147
148    public static final String TYPE_TAGGING = "Tagging";
149
150    public static final String RELATION_TABLE = "relation";
151
152    public static final String ECM_SIMPLE_ACP_PRINCIPAL = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_PRINCIPAL;
153
154    public static final String ECM_SIMPLE_ACP_PERMISSION = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_PERMISSION;
155
156    public static final String ECM_SIMPLE_ACP_GRANT = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_GRANT;
157
158    public static final String ECM_SIMPLE_ACP_NAME = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_NAME;
159
160    public static final String ECM_SIMPLE_ACP_POS = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_POS;
161
162    /**
163     * @since 7.4
164     */
165    public static final String ECM_SIMPLE_ACP_CREATOR = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_CREATOR;
166
167    /**
168     * @since 7.4
169     */
170    public static final String ECM_SIMPLE_ACP_BEGIN = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_BEGIN;
171
172    /**
173     * @since 7.4
174     */
175    public static final String ECM_SIMPLE_ACP_END = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_END;
176
177    /**
178     * @since 7.4
179     */
180    public static final String ECM_SIMPLE_ACP_STATUS = NXQL.ECM_ACL + "/*/" + NXQL.ECM_ACL_STATUS;
181
182    public static final String ECM_TAG_STAR = NXQL.ECM_TAG + "/*";
183
184    protected static final String TABLE_HIER_ALIAS = "_H";
185
186    protected static final String TABLE_FRAG_ALIAS = "_F";
187
188    protected static final String SUBQUERY_ARRAY_ALIAS = "_A";
189
190    protected static final String COL_ALIAS_PREFIX = "_C";
191
192    protected static final String UNION_ALIAS = "_T";
193
194    protected static final String WITH_ALIAS_PREFIX = "_W";
195
196    protected static final String READ_ACL_ALIAS = "_RACL";
197
198    protected static final String READ_ACL_USER_MAP_ALIAS = "_ACLRUSERMAP";
199
200    protected static final String DATE_CAST = "DATE";
201
202    protected static final String COUNT_FUNCTION = "COUNT";
203
204    protected static final String AVG_FUNCTION = "AVG";
205
206    protected static final List<String> AGGREGATE_FUNCTIONS = Arrays.asList(COUNT_FUNCTION, AVG_FUNCTION, "SUM", "MIN",
207            "MAX");
208
209    /*
210     * Fields used by the search service.
211     */
212
213    protected SQLInfo sqlInfo;
214
215    protected Database database;
216
217    protected Dialect dialect;
218
219    protected Model model;
220
221    protected Set<String> neverPerInstanceMixins;
222
223    protected PathResolver pathResolver;
224
225    protected final Map<String, String> aliasesByName = new HashMap<String, String>();
226
227    protected final List<String> aliases = new LinkedList<String>();
228
229    /**
230     * Whether the query must match only proxies (TRUE), no proxies (FALSE), or not specified (null).
231     */
232    protected Boolean proxyClause;
233
234    /** The reason why proxyClause was set to non-null. */
235    protected String proxyClauseReason;
236
237    // The hierarchy table for the hierarchy/name, may be an alias table
238    protected Table hierTable;
239
240    // The hierarchy table of the data
241    protected Table dataHierTable;
242
243    // The proxies table if querying for proxies
244    protected Table proxyTable;
245
246    protected List<Join> joins;
247
248    protected List<String> whereClauses;
249
250    protected List<Serializable> whereParams;
251
252    // fragmentName or prefix/fragmentName -> fragment table to join
253    protected Map<String, Table> propertyFragmentTables = new HashMap<String, Table>();
254
255    protected int fragJoinCount = 0;
256
257    @Override
258    public String getName() {
259        return NXQL.NXQL;
260    }
261
262    @Override
263    public boolean accepts(String queryType) {
264        return queryType.equals(NXQL.NXQL);
265    }
266
267    public enum DocKind {
268        DIRECT, PROXY;
269    }
270
271    @Override
272    public Query buildQuery(SQLInfo sqlInfo, Model model, PathResolver pathResolver, String query,
273            QueryFilter queryFilter, Object... params) {
274        this.sqlInfo = sqlInfo;
275        database = sqlInfo.database;
276        dialect = sqlInfo.dialect;
277        this.model = model;
278        this.pathResolver = pathResolver;
279        neverPerInstanceMixins = new HashSet<String>(model.getNoPerDocumentQueryFacets());
280        // compat
281        Set<String> npim = model.getRepositoryDescriptor().neverPerInstanceMixins;
282        if (npim != null) {
283            neverPerInstanceMixins.addAll(npim);
284        }
285
286        // transform the query according to the transformers defined by the
287        // security policies
288        SQLQuery sqlQuery = SQLQueryParser.parse(query);
289        for (SQLQuery.Transformer transformer : queryFilter.getQueryTransformers()) {
290            sqlQuery = transformer.transform(queryFilter.getPrincipal(), sqlQuery);
291        }
292
293        // SELECT * -> SELECT ecm:uuid
294        SelectClause selectClause = sqlQuery.select;
295        if (selectClause.isEmpty()) {
296            // turned into SELECT ecm:uuid
297            selectClause.add(new Reference(NXQL.ECM_UUID));
298        }
299        boolean selectStar = selectClause.getSelectList().size() == 1
300                && (selectClause.get(0).equals(new Reference(NXQL.ECM_UUID)));
301
302        /*
303         * Analyze query to find all relevant types and keys for the criteria, and fulltext matches.
304         */
305
306        QueryAnalyzer queryAnalyzer = newQueryAnalyzer(queryFilter.getFacetFilter());
307        try {
308            queryAnalyzer.visitQuery(sqlQuery);
309        } catch (QueryCannotMatchException e) {
310            // query cannot match
311            return null;
312        }
313
314        boolean distinct = selectClause.isDistinct();
315        if (selectStar && queryAnalyzer.hasWildcardIndex) {
316            distinct = true;
317        }
318
319        boolean reAnalyze = false;
320        // add a default ORDER BY ecm:fulltextScore DESC
321        if (queryAnalyzer.ftCount == 1 && !distinct && sqlQuery.orderBy == null) {
322            sqlQuery.orderBy = new OrderByClause(
323                    new OrderByList(new OrderByExpr(new Reference(NXQL.ECM_FULLTEXT_SCORE), true)), false);
324            queryAnalyzer.orderByScore = true;
325            reAnalyze = true;
326        }
327        // if ORDER BY ecm:fulltextScore, make sure we SELECT on it too
328        if (queryAnalyzer.orderByScore && !queryAnalyzer.selectScore) {
329            selectClause.add(new Reference(NXQL.ECM_FULLTEXT_SCORE));
330            reAnalyze = true;
331        }
332        if (reAnalyze) {
333            queryAnalyzer.visitQuery(sqlQuery);
334        }
335
336        if (queryAnalyzer.ftCount > 1 && (queryAnalyzer.orderByScore || queryAnalyzer.selectScore)) {
337            throw new QueryParseException(
338                    "Cannot use " + NXQL.ECM_FULLTEXT_SCORE + " with more than one fulltext match expression");
339        }
340
341        /*
342         * Find whether to check proxies, relations.
343         */
344
345        if (!model.getRepositoryDescriptor().getProxiesEnabled()) {
346            if (proxyClause == Boolean.TRUE) {
347                throw new QueryParseException(
348                        "Proxies are disabled by configuration, a query with " + proxyClauseReason + " is disallowed");
349            }
350            proxyClause = Boolean.FALSE;
351        }
352        if (queryAnalyzer.onlyRelations) {
353            if (proxyClause == Boolean.TRUE) {
354                // no proxies to relations, query cannot match
355                return null;
356            }
357            proxyClause = Boolean.FALSE;
358        }
359        DocKind[] docKinds;
360        if (proxyClause == Boolean.TRUE) {
361            docKinds = new DocKind[] { DocKind.PROXY };
362        } else if (proxyClause == Boolean.FALSE) {
363            docKinds = new DocKind[] { DocKind.DIRECT };
364        } else {
365            docKinds = new DocKind[] { DocKind.DIRECT, DocKind.PROXY };
366        }
367        boolean doUnion = docKinds.length > 1;
368
369        /*
370         * DISTINCT check and add additional selected columns for ORDER BY.
371         */
372
373        Set<String> onlyOrderByColumnNames = new HashSet<String>(queryAnalyzer.orderByColumnNames);
374        onlyOrderByColumnNames.removeAll(queryAnalyzer.whatColumnNames);
375
376        if (doUnion || distinct) {
377            // if UNION, we need all the ORDER BY columns in the SELECT list
378            // for aliasing
379            if (distinct && !onlyOrderByColumnNames.isEmpty()) {
380                // if DISTINCT, check that the ORDER BY columns are all in the
381                // SELECT list
382                if (!selectStar) {
383                    throw new QueryParseException(
384                            "For SELECT DISTINCT the ORDER BY columns must be in the SELECT list, missing: "
385                                    + onlyOrderByColumnNames);
386                }
387                // for a SELECT *, we can add the needed columns if they
388                // don't involve wildcard index array elements
389                if (queryAnalyzer.orderByHasWildcardIndex) {
390                    throw new QueryParseException("For SELECT * the ORDER BY columns cannot use wildcard indexes");
391                }
392            }
393            for (String name : onlyOrderByColumnNames) {
394                selectClause.add(new Reference(name));
395            }
396        }
397
398        /*
399         * Build the FROM / JOIN criteria for each select.
400         */
401
402        String from;
403        List<Column> whatColumns = null;
404        List<String> whatKeys = null;
405        Select select = null;
406        String orderBy = null;
407        List<String> statements = new ArrayList<String>(2);
408        List<Serializable> selectParams = new LinkedList<Serializable>();
409        List<String> withTables = new LinkedList<String>();
410        List<Select> withSelects = new LinkedList<Select>();
411        List<String> withSelectsStatements = new LinkedList<String>();
412        List<Serializable> withParams = new LinkedList<Serializable>();
413        Table hier = database.getTable(model.HIER_TABLE_NAME);
414
415        for (DocKind docKind : docKinds) {
416
417            // Quoted id in the hierarchy. This is the id returned by the query.
418            String hierId;
419
420            joins = new LinkedList<Join>();
421            whereClauses = new LinkedList<String>();
422            whereParams = new LinkedList<Serializable>();
423            propertyFragmentTables = new HashMap<String, Table>();
424            fragJoinCount = 0;
425
426            switch (docKind) {
427            case DIRECT:
428                hierTable = hier;
429                dataHierTable = hierTable;
430                hierId = hierTable.getColumn(model.MAIN_KEY).getFullQuotedName();
431                from = hierTable.getQuotedName();
432                proxyTable = null;
433                break;
434            case PROXY:
435                hierTable = new TableAlias(hier, TABLE_HIER_ALIAS);
436                dataHierTable = hier;
437                // TODO use dialect
438                from = hier.getQuotedName() + " " + hierTable.getQuotedName();
439                hierId = hierTable.getColumn(model.MAIN_KEY).getFullQuotedName();
440                // proxies
441                proxyTable = database.getTable(model.PROXY_TABLE_NAME);
442                // join all that
443                addJoin(Join.INNER, null, proxyTable, model.MAIN_KEY, hierTable, model.MAIN_KEY, null, -1, null);
444                addJoin(Join.INNER, null, dataHierTable, model.MAIN_KEY, proxyTable, model.PROXY_TARGET_KEY, null, -1,
445                        null);
446                break;
447            default:
448                throw new AssertionError(docKind);
449            }
450            fixInitialJoins();
451
452            // init builder
453
454            WhereBuilder whereBuilder = newWhereBuilder(docKind == DocKind.PROXY);
455            selectClause.accept(whereBuilder);
456            whatColumns = whereBuilder.whatColumns;
457            whatKeys = whereBuilder.whatKeys;
458
459            /*
460             * Process WHERE.
461             */
462
463            if (queryAnalyzer.wherePredicate != null) {
464                queryAnalyzer.wherePredicate.accept(whereBuilder);
465                // WHERE clause
466                String where = whereBuilder.buf.toString();
467                if (where.length() != 0) {
468                    whereClauses.add(where);
469                }
470            }
471
472            /*
473             * Process WHAT to select.
474             */
475
476            // alias columns in all cases to simplify logic
477            List<String> whatNames = new ArrayList<String>(1);
478            List<Serializable> whatNamesParams = new ArrayList<Serializable>(1);
479            String mainAlias = hierId;
480            aliasesByName.clear();
481            aliases.clear();
482            for (int i = 0; i < whatColumns.size(); i++) {
483                Column col = whatColumns.get(i);
484                String key = whatKeys.get(i);
485                String alias;
486                String whatName;
487                if (NXQL.ECM_FULLTEXT_SCORE.equals(key)) {
488                    FulltextMatchInfo ftMatchInfo = whereBuilder.ftMatchInfo;
489                    if (ftMatchInfo == null) {
490                        throw new QueryParseException(
491                                NXQL.ECM_FULLTEXT_SCORE + " cannot be used without " + NXQL.ECM_FULLTEXT);
492                    }
493                    alias = ftMatchInfo.scoreAlias;
494                    whatName = ftMatchInfo.scoreExpr;
495                    if (ftMatchInfo.scoreExprParam != null) {
496                        whatNamesParams.add(ftMatchInfo.scoreExprParam);
497                    }
498                } else {
499                    alias = dialect.openQuote() + COL_ALIAS_PREFIX + (i + 1) + dialect.closeQuote();
500                    whatName = getSelectColName(col, key);
501                    if (col.getTable().getRealTable() == hier && col.getKey().equals(model.MAIN_KEY)) {
502                        mainAlias = alias;
503                    }
504                }
505                aliasesByName.put(key, alias);
506                aliases.add(alias);
507                whatNames.add(whatName + " AS " + alias);
508            }
509
510            fixWhatColumns(whatColumns);
511
512            /*
513             * Process ORDER BY.
514             */
515
516            // ORDER BY computed just once; may use just aliases
517            if (orderBy == null) {
518                if (sqlQuery.orderBy != null) {
519                    // needs aliasesByName
520                    whereBuilder.aliasOrderByColumns = doUnion;
521                    whereBuilder.buf.setLength(0);
522                    sqlQuery.orderBy.accept(whereBuilder);
523                    // ends up in WhereBuilder#visitOrderByExpr
524                    orderBy = whereBuilder.buf.toString();
525                }
526            }
527
528            String selectWhat = StringUtils.join(whatNames, ", ");
529            if (!doUnion && distinct) {
530                selectWhat = "DISTINCT " + selectWhat;
531            }
532
533            /*
534             * Soft delete.
535             */
536
537            if (model.getRepositoryDescriptor().getSoftDeleteEnabled()) {
538                whereClauses.add(hierTable.getColumn(model.MAIN_IS_DELETED_KEY).getFullQuotedName() + " IS NULL");
539            }
540
541            /*
542             * Security check.
543             */
544
545            String securityClause = null;
546            List<Serializable> securityParams = new LinkedList<Serializable>();
547            List<Join> securityJoins = new ArrayList<Join>(2);
548            if (queryFilter.getPrincipals() != null) {
549                Serializable principals = queryFilter.getPrincipals();
550                Serializable permissions = queryFilter.getPermissions();
551                if (!dialect.supportsArrays()) {
552                    principals = StringUtils.join((String[]) principals, Dialect.ARRAY_SEP);
553                    permissions = StringUtils.join((String[]) permissions, Dialect.ARRAY_SEP);
554                }
555                // when using WITH for the query, the main column is referenced
556                // through an alias because of the subselect
557                String id = dialect.supportsWith() ? mainAlias : hierId;
558                if (dialect.supportsReadAcl()) {
559                    /* optimized read acl */
560                    // JOIN hierarchy_read_acl _RACL ON hierarchy.id = _RACL.id
561                    // JOIN aclr_user_map _ACLRUSERMAP ON _RACL.acl_id =
562                    // _ACLRUSERMAP.acl_id
563                    // WHERE _ACLRUSERMAP.user_id = md5('bob,Everyone')
564                    String racl = dialect.openQuote() + READ_ACL_ALIAS + dialect.closeQuote();
565                    String aclrum = dialect.openQuote() + READ_ACL_USER_MAP_ALIAS + dialect.closeQuote();
566                    securityJoins.add(new Join(Join.INNER, Model.HIER_READ_ACL_TABLE_NAME, READ_ACL_ALIAS, null, id,
567                            racl + '.' + Model.HIER_READ_ACL_ID));
568                    securityJoins.add(new Join(Join.INNER, Model.ACLR_USER_MAP_TABLE_NAME, READ_ACL_USER_MAP_ALIAS,
569                            null, racl + '.' + Model.HIER_READ_ACL_ACL_ID, aclrum + '.' + Model.ACLR_USER_MAP_ACL_ID));
570                    securityClause = dialect.getReadAclsCheckSql(aclrum + '.' + Model.ACLR_USER_MAP_USER_ID);
571                    securityParams.add(principals);
572                } else {
573                    securityClause = dialect.getSecurityCheckSql(id);
574                    securityParams.add(principals);
575                    securityParams.add(permissions);
576                }
577            }
578
579            /*
580             * Resulting select.
581             */
582
583            if (securityClause != null) {
584                if (dialect.supportsWith()) {
585                    // wrap security into a WITH
586                    String withTable = dialect.openQuote() + WITH_ALIAS_PREFIX + (statements.size() + 1)
587                            + dialect.closeQuote();
588                    withTables.add(withTable);
589                    Select withSelect = new Select(null);
590                    withSelect.setWhat("*");
591                    String withFrom = withTable;
592                    for (Join j : securityJoins) {
593                        withFrom += j.toSql(dialect);
594                    }
595                    withSelect.setFrom(withFrom);
596                    withSelect.setWhere(securityClause);
597                    withSelects.add(withSelect);
598                    withSelectsStatements.add(withSelect.getStatement());
599                    withParams.addAll(securityParams);
600                } else {
601                    // add directly to main select
602                    joins.addAll(securityJoins);
603                    whereClauses.add(securityClause);
604                    whereParams.addAll(securityParams);
605                }
606            }
607
608            select = new Select(null);
609            select.setWhat(selectWhat);
610            selectParams.addAll(whatNamesParams);
611
612            StringBuilder fromb = new StringBuilder(from);
613            if (dialect.needsOracleJoins() && doUnion && !securityJoins.isEmpty() && queryAnalyzer.ftCount != 0) {
614                // NXP-5410 we must use Oracle joins
615                // when there's union all + fulltext + security
616                for (Join join : joins) {
617                    if (!join.whereClauses.isEmpty()) {
618                        // we cannot use Oracle join when there are join filters
619                        throw new QueryParseException("Query too complex for Oracle (NXP-5410)");
620                    }
621                }
622                // implicit joins for Oracle
623                List<String> joinClauses = new LinkedList<String>();
624                for (Join join : joins) {
625                    fromb.append(", ");
626                    fromb.append(join.getTable(dialect));
627                    if (join.tableParam != null) {
628                        selectParams.add(join.tableParam);
629                    }
630                    String joinClause = join.getClause(dialect);
631                    if (join.kind == Join.LEFT) {
632                        joinClause += "(+)"; // Oracle implicit LEFT JOIN syntax
633                    }
634                    if (!join.whereClauses.isEmpty()) {
635                        joinClause += " AND " + StringUtils.join(join.whereClauses, " AND ");
636                        selectParams.addAll(join.whereParams);
637                    }
638                    joinClauses.add(joinClause);
639                }
640                whereClauses.addAll(0, joinClauses);
641            } else {
642                // else ANSI join
643                Collections.sort(joins); // implicit JOINs last (PostgreSQL)
644                for (Join join : joins) {
645                    if (join.tableParam != null) {
646                        selectParams.add(join.tableParam);
647                    }
648                    String joinClause = join.toSql(dialect);
649                    // add JOIN filter for complex properties
650                    if (!join.whereClauses.isEmpty()) {
651                        joinClause += " AND " + StringUtils.join(join.whereClauses, " AND ");
652                        selectParams.addAll(join.whereParams);
653                    }
654                    fromb.append(joinClause);
655                }
656            }
657
658            select.setFrom(fromb.toString());
659            select.setWhere(StringUtils.join(whereClauses, " AND "));
660            selectParams.addAll(whereParams);
661
662            statements.add(select.getStatement());
663        }
664
665        /*
666         * Create the whole select.
667         */
668
669        if (doUnion) {
670            select = new Select(null);
671            // use aliases for column names
672            String selectWhat = StringUtils.join(aliases, ", ");
673            if (distinct) {
674                selectWhat = "DISTINCT " + selectWhat;
675            }
676            select.setWhat(selectWhat);
677            // note that Derby has bizarre restrictions on parentheses placement
678            // around UNION, see http://issues.apache.org/jira/browse/DERBY-2374
679            String subselect;
680            if (withSelects.isEmpty()) {
681                subselect = StringUtils.join(statements, " UNION ALL ");
682            } else {
683                StringBuilder with = new StringBuilder("WITH ");
684                for (int i = 0; i < statements.size(); i++) {
685                    if (i > 0) {
686                        with.append(", ");
687                    }
688                    with.append(withTables.get(i));
689                    with.append(" AS (");
690                    with.append(statements.get(i));
691                    with.append(')');
692                }
693                with.append(' ');
694                subselect = with.toString() + StringUtils.join(withSelectsStatements, " UNION ALL ");
695                selectParams.addAll(withParams);
696            }
697            String selectFrom = '(' + subselect + ')';
698            if (dialect.needsAliasForDerivedTable()) {
699                selectFrom += " AS " + dialect.openQuote() + UNION_ALIAS + dialect.closeQuote();
700            }
701            select.setFrom(selectFrom);
702        } else {
703            // use last (and only) Select in above big loop
704            if (!withSelects.isEmpty()) {
705                select = new Select(null);
706                String with = withTables.get(0) + " AS (" + statements.get(0) + ')';
707                select.setWith(with);
708                Select withSelect = withSelects.get(0);
709                select.setWhat(withSelect.getWhat());
710                select.setFrom(withSelect.getFrom());
711                select.setWhere(withSelect.getWhere());
712                selectParams.addAll(withParams);
713            }
714        }
715
716        select.setOrderBy(orderBy);
717        fixSelect(select);
718
719        Query q = new Query();
720        ColumnMapMaker mapMaker = new ColumnMapMaker(whatColumns, whatKeys);
721        q.selectInfo = new SQLInfoSelect(select.getStatement(), whatColumns, mapMaker, null, null);
722        q.selectParams = selectParams;
723        return q;
724    }
725
726    // overridden by specialized query makers that need to tweak some joins
727    protected void addJoin(int kind, String alias, Table table, String column, Table contextTable, String contextColumn,
728            String name, int index, String primaryType) {
729        Column column1 = contextTable.getColumn(contextColumn);
730        Column column2 = table.getColumn(column);
731        Join join = new Join(kind, table.getRealTable().getQuotedName(), alias, null, column1, column2);
732        if (name != null) {
733            String nameCol = table.getColumn(model.HIER_CHILD_NAME_KEY).getFullQuotedName();
734            join.addWhereClause(nameCol + " = ?", name);
735        }
736        if (index != -1) {
737            String posCol = table.getColumn(model.HIER_CHILD_POS_KEY).getFullQuotedName();
738            join.addWhereClause(posCol + " = ?", Long.valueOf(index));
739        }
740        if (primaryType != null) {
741            String typeCol = table.getColumn(model.MAIN_PRIMARY_TYPE_KEY).getFullQuotedName();
742            join.addWhereClause(typeCol + " = ?", primaryType);
743        }
744        joins.add(join);
745    }
746
747    /**
748     * Gets the table for the given fragmentName in the given contextKey, and maybe adds a join if one is not already
749     * done.
750     * <p>
751     * LEFT JOIN fragmentName _F123 ON contextHier.id = _F123.id
752     */
753    protected Table getFragmentTable(Table contextHier, String contextKey, String fragmentName, int index,
754            boolean skipJoin) {
755        return getFragmentTable(Join.LEFT, contextHier, contextKey, fragmentName, model.MAIN_KEY, index, skipJoin,
756                null);
757    }
758
759    /**
760     * Adds a more general JOIN:
761     * <p>
762     * (LEFT) JOIN fragmentName _F123 ON contextTable.id = _F123.fragmentColumn
763     */
764    protected Table getFragmentTable(int joinKind, Table contextTable, String contextKey, String fragmentName,
765            String fragmentColumn, int index, boolean skipJoin, String primaryType) {
766        Table table = propertyFragmentTables.get(contextKey);
767        if (table == null) {
768            Table baseTable = database.getTable(fragmentName);
769            String alias = TABLE_FRAG_ALIAS + ++fragJoinCount;
770            table = new TableAlias(baseTable, alias);
771            propertyFragmentTables.put(contextKey, table);
772            if (!skipJoin) {
773                addJoin(joinKind, alias, table, fragmentColumn, contextTable, model.MAIN_KEY, null, index, primaryType);
774            }
775        }
776        return table;
777    }
778
779    // overridden by specialized query makers that need to tweak some joins
780    protected void fixInitialJoins() {
781        // to be overridden
782    }
783
784    // overridden by specialized query makers that need to add COUNT
785    protected String getSelectColName(Column col) {
786        return col.getFullQuotedName();
787    }
788
789    /** key used to extract array index if needed */
790    protected String getSelectColName(Column col, String key) {
791        String colName = getSelectColName(col);
792        if (col.isArray()) {
793            String[] segments = canonicalXPath(key).split("/");
794            if (segments.length > 1) {
795                // last segment
796                String segment = segments[segments.length - 1];
797                if (INDEX.matcher(segment).matches() && !segment.startsWith("*")) {
798                    int arrayElementIndex = Integer.parseInt(segment);
799                    colName = dialect.getArrayElementString(colName, arrayElementIndex);
800                }
801            }
802        }
803        return colName;
804    }
805
806    // overridden by specialized query makers that need to add COUNT
807    protected void fixWhatColumns(List<Column> whatColumns) {
808        // to be overridden
809    }
810
811    // overridden by specialized query makers that need to add GROUP BY
812    protected void fixSelect(Select select) {
813        // to be overridden
814    }
815
816    protected static boolean findFulltextIndexOrField(Model model, String[] nameref) {
817        boolean useIndex;
818        String name = nameref[0];
819        if (name.equals(NXQL.ECM_FULLTEXT)) {
820            name = Model.FULLTEXT_DEFAULT_INDEX;
821            useIndex = true;
822        } else {
823            // ecm:fulltext_indexname
824            // ecm:fulltext.field
825            char sep = name.charAt(NXQL.ECM_FULLTEXT.length());
826            if (sep != '.' && sep != '_') {
827                throw new QueryParseException("Unknown field: " + name);
828            }
829            useIndex = sep == '_';
830            name = name.substring(NXQL.ECM_FULLTEXT.length() + 1);
831            if (useIndex) {
832                if (!model.getFulltextConfiguration().indexNames.contains(name)) {
833                    throw new QueryParseException("No such fulltext index: " + name);
834                }
835            } else {
836                // find if there's an index holding just that field
837                String index = model.getFulltextConfiguration().fieldToIndexName.get(name);
838                if (index != null) {
839                    name = index;
840                    useIndex = true;
841                }
842            }
843        }
844        nameref[0] = name;
845        return useIndex;
846    }
847
848    // digits or star or star followed by digits, for segments
849    protected final static Pattern INDEX = Pattern.compile("\\d+|\\*|\\*\\d+");
850
851    // wildcard index in xpath
852    protected final static Pattern HAS_WILDCARD_INDEX = Pattern.compile(".*/(\\*|\\*\\d+)(/.*|$)");
853
854    // digits or star or star followed by digits, then slash, for replaceAll
855    protected final static Pattern INDEX_SLASH = Pattern.compile("/(?:\\d+|\\*|\\*\\d+)(/|$)");
856
857    // non-canonical index syntax, for replaceAll
858    protected final static Pattern NON_CANON_INDEX = Pattern.compile("[^/\\[\\]]+" // name
859            + "\\[(\\d+|\\*|\\*\\d+)\\]" // index in brackets
860    );
861
862    /**
863     * Canonicalizes a Nuxeo-xpath.
864     * <p>
865     * Replaces {@code a/foo[123]/b} with {@code a/123/b}
866     * <p>
867     * A star or a star followed by digits can be used instead of just the digits as well.
868     *
869     * @param xpath the xpath
870     * @return the canonicalized xpath.
871     */
872    public static String canonicalXPath(String xpath) {
873        while (xpath.length() > 0 && xpath.charAt(0) == '/') {
874            xpath = xpath.substring(1);
875        }
876        if (xpath.indexOf('[') == -1) {
877            return xpath;
878        } else {
879            return NON_CANON_INDEX.matcher(xpath).replaceAll("$1");
880        }
881    }
882
883    /**
884     * Turns the xpath into one where all indices have been replaced by *.
885     *
886     * @param xpath the xpath
887     * @return the simple xpath
888     */
889    public static String simpleXPath(String xpath) {
890        xpath = canonicalXPath(xpath);
891        return INDEX_SLASH.matcher(xpath).replaceAll("/*$1");
892    }
893
894    public boolean hasWildcardIndex(String xpath) {
895        xpath = canonicalXPath(xpath);
896        return HAS_WILDCARD_INDEX.matcher(xpath).matches();
897    }
898
899    protected QueryAnalyzer newQueryAnalyzer(FacetFilter facetFilter) {
900        return new QueryAnalyzer(facetFilter);
901    }
902
903    protected static Set<String> getStringLiterals(LiteralList list) {
904        Set<String> set = new HashSet<String>();
905        for (Literal literal : list) {
906            if (!(literal instanceof StringLiteral)) {
907                throw new QueryParseException("requires string literals");
908            }
909            set.add(((StringLiteral) literal).value);
910        }
911        return set;
912    }
913
914    protected static Serializable getSerializableLiteral(Literal literal) {
915        Serializable value;
916        if (literal instanceof BooleanLiteral) {
917            value = Boolean.valueOf(((BooleanLiteral) literal).value);
918        } else if (literal instanceof DateLiteral) {
919            DateLiteral dLit = (DateLiteral) literal;
920            value = dLit.onlyDate ? dLit.toSqlDate() : dLit.toCalendar();
921        } else if (literal instanceof DoubleLiteral) {
922            value = Double.valueOf(((DoubleLiteral) literal).value);
923        } else if (literal instanceof IntegerLiteral) {
924            value = Long.valueOf(((IntegerLiteral) literal).value);
925        } else if (literal instanceof StringLiteral) {
926            value = ((StringLiteral) literal).value;
927        } else {
928            throw new QueryParseException("type of literal in list is not recognized: " + literal.getClass());
929        }
930        return value;
931    }
932
933    protected static List<Serializable> getSerializableLiterals(LiteralList list) {
934        List<Serializable> serList = new ArrayList<Serializable>(list.size());
935        for (Literal literal : list) {
936            serList.add(getSerializableLiteral(literal));
937        }
938        return serList;
939    }
940
941    /**
942     * Collects various info about the query AST, and rewrites the toplevel AND {@link Predicate}s of the WHERE clause
943     * into a single {@link MultiExpression} for easier analysis.
944     */
945    protected class QueryAnalyzer extends DefaultQueryVisitor {
946
947        private static final long serialVersionUID = 1L;
948
949        protected FacetFilter facetFilter;
950
951        protected boolean inSelect;
952
953        protected boolean inOrderBy;
954
955        protected LinkedList<Operand> toplevelOperands;
956
957        protected MultiExpression wherePredicate;
958
959        /** Do we match only relations (and therefore no proxies). */
960        protected boolean onlyRelations;
961
962        protected List<String> whatColumnNames;
963
964        protected List<String> orderByColumnNames;
965
966        protected boolean hasWildcardIndex;
967
968        protected boolean orderByHasWildcardIndex;
969
970        protected int ftCount;
971
972        protected boolean selectScore;
973
974        protected boolean orderByScore;
975
976        public QueryAnalyzer(FacetFilter facetFilter) {
977            this.facetFilter = facetFilter;
978        }
979
980        protected void init() {
981            toplevelOperands = new LinkedList<Operand>();
982            whatColumnNames = new LinkedList<String>();
983            orderByColumnNames = new LinkedList<String>();
984            hasWildcardIndex = false;
985            orderByHasWildcardIndex = false;
986            ftCount = 0;
987            selectScore = false;
988            orderByScore = false;
989        }
990
991        @Override
992        public void visitQuery(SQLQuery node) {
993            init();
994            if (facetFilter != null) {
995                addFacetFilterClauses(facetFilter);
996            }
997            visitSelectClause(node.select);
998            visitFromClause(node.from);
999            visitWhereClause(node.where); // may be null
1000            if (node.orderBy != null) {
1001                visitOrderByClause(node.orderBy);
1002            }
1003        }
1004
1005        public void addFacetFilterClauses(FacetFilter facetFilter) {
1006            for (String mixin : facetFilter.required) {
1007                // every facet is required, not just any of them,
1008                // so do them one by one
1009                // expr = getMixinsMatchExpression(Collections.singleton(facet),
1010                // true);
1011                Expression expr = new Expression(new Reference(NXQL.ECM_MIXINTYPE), Operator.EQ,
1012                        new StringLiteral(mixin));
1013                toplevelOperands.add(expr);
1014            }
1015            if (!facetFilter.excluded.isEmpty()) {
1016                // expr = getMixinsMatchExpression(facetFilter.excluded, false);
1017                LiteralList list = new LiteralList();
1018                for (String mixin : facetFilter.excluded) {
1019                    list.add(new StringLiteral(mixin));
1020                }
1021                Expression expr = new Expression(new Reference(NXQL.ECM_MIXINTYPE), Operator.NOTIN, list);
1022                toplevelOperands.add(expr);
1023            }
1024        }
1025
1026        @Override
1027        public void visitSelectClause(SelectClause node) {
1028            inSelect = true;
1029            super.visitSelectClause(node);
1030            inSelect = false;
1031        }
1032
1033        /**
1034         * Finds all the types to take into account (all concrete types being a subtype of the passed types) based on
1035         * the FROM list.
1036         * <p>
1037         * Adds them as a ecm:primaryType match in the toplevel operands.
1038         */
1039        @Override
1040        public void visitFromClause(FromClause node) {
1041            onlyRelations = true;
1042            Set<String> fromTypes = new HashSet<String>();
1043            FromList elements = node.elements;
1044            for (int i = 0; i < elements.size(); i++) {
1045                String typeName = elements.get(i);
1046                if (TYPE_DOCUMENT.equalsIgnoreCase(typeName)) {
1047                    typeName = TYPE_DOCUMENT;
1048                }
1049                Set<String> subTypes = model.getDocumentSubTypes(typeName);
1050                if (subTypes == null) {
1051                    throw new QueryParseException("Unknown type: " + typeName);
1052                }
1053                fromTypes.addAll(subTypes);
1054                boolean isRelation = false;
1055                do {
1056                    if (TYPE_RELATION.equals(typeName)) {
1057                        isRelation = true;
1058                        break;
1059                    }
1060                    typeName = model.getDocumentSuperType(typeName);
1061                } while (typeName != null);
1062                onlyRelations = onlyRelations && isRelation;
1063            }
1064            fromTypes.remove(model.ROOT_TYPE);
1065            LiteralList list = new LiteralList();
1066            for (String type : fromTypes) {
1067                list.add(new StringLiteral(type));
1068            }
1069            toplevelOperands.add(new Expression(new Reference(NXQL.ECM_PRIMARYTYPE), Operator.IN, list));
1070        }
1071
1072        @Override
1073        public void visitWhereClause(WhereClause node) {
1074            if (node != null) {
1075                analyzeToplevelOperands(node.predicate);
1076            }
1077            simplifyToplevelOperands();
1078            wherePredicate = new MultiExpression(Operator.AND, toplevelOperands);
1079            super.visitMultiExpression(wherePredicate);
1080        }
1081
1082        /**
1083         * Process special toplevel ANDed operands: ecm:isProxy
1084         */
1085        protected void analyzeToplevelOperands(Operand node) {
1086            if (node instanceof Expression) {
1087                Expression expr = (Expression) node;
1088                Operator op = expr.operator;
1089                if (op == Operator.AND) {
1090                    analyzeToplevelOperands(expr.lvalue);
1091                    analyzeToplevelOperands(expr.rvalue);
1092                    return;
1093                }
1094                if (op == Operator.EQ || op == Operator.NOTEQ) {
1095                    // put reference on the left side
1096                    if (expr.rvalue instanceof Reference) {
1097                        expr = new Expression(expr.rvalue, op, expr.lvalue);
1098                    }
1099                    if (expr.lvalue instanceof Reference) {
1100                        String name = ((Reference) expr.lvalue).name;
1101                        if (NXQL.ECM_ISPROXY.equals(name)) {
1102                            analyzeToplevelIsProxy(expr);
1103                            return;
1104                        } else if (NXQL.ECM_PROXY_TARGETID.equals(name) || NXQL.ECM_PROXY_VERSIONABLEID.equals(name)) {
1105                            analyzeToplevelProxyProperty(expr);
1106                            // no return, we want the node
1107                        }
1108                    }
1109                }
1110            }
1111            toplevelOperands.add(node);
1112        }
1113
1114        /**
1115         * Simplify ecm:primaryType positive references, and non-per-instance mixin types.
1116         */
1117        protected void simplifyToplevelOperands() {
1118            Set<String> primaryTypes = null; // if defined, required
1119            for (Iterator<Operand> it = toplevelOperands.iterator(); it.hasNext();) {
1120                // whenever we don't know how to optimize the expression,
1121                // we just continue the loop
1122                Operand node = it.next();
1123                if (!(node instanceof Expression)) {
1124                    continue;
1125                }
1126                Expression expr = (Expression) node;
1127                if (!(expr.lvalue instanceof Reference)) {
1128                    continue;
1129                }
1130                String name = ((Reference) expr.lvalue).name;
1131                Operator op = expr.operator;
1132                Operand rvalue = expr.rvalue;
1133                if (NXQL.ECM_PRIMARYTYPE.equals(name)) {
1134                    if (op != Operator.EQ && op != Operator.IN) {
1135                        continue;
1136                    }
1137                    Set<String> set;
1138                    if (op == Operator.EQ) {
1139                        if (!(rvalue instanceof StringLiteral)) {
1140                            continue;
1141                        }
1142                        String primaryType = ((StringLiteral) rvalue).value;
1143                        set = new HashSet<String>(Collections.singleton(primaryType));
1144                    } else { // Operator.IN
1145                        if (!(rvalue instanceof LiteralList)) {
1146                            continue;
1147                        }
1148                        set = getStringLiterals((LiteralList) rvalue);
1149                    }
1150                    if (primaryTypes == null) {
1151                        primaryTypes = set;
1152                    } else {
1153                        primaryTypes.retainAll(set);
1154                    }
1155                    it.remove(); // expression simplified into primaryTypes set
1156                } else if (NXQL.ECM_MIXINTYPE.equals(name)) {
1157                    if (op != Operator.EQ && op != Operator.NOTEQ) {
1158                        continue;
1159                    }
1160                    if (!(rvalue instanceof StringLiteral)) {
1161                        continue;
1162                    }
1163                    String mixin = ((StringLiteral) rvalue).value;
1164                    if (!neverPerInstanceMixins.contains(mixin)) {
1165                        // mixin per instance -> primary type checks not enough
1166                        continue;
1167                    }
1168                    Set<String> set = model.getMixinDocumentTypes(mixin);
1169                    if (primaryTypes == null) {
1170                        if (op == Operator.EQ) {
1171                            primaryTypes = new HashSet<String>(set); // copy
1172                        } else {
1173                            continue; // unknown positive, no optimization
1174                        }
1175                    } else {
1176                        if (op == Operator.EQ) {
1177                            primaryTypes.retainAll(set);
1178                        } else {
1179                            primaryTypes.removeAll(set);
1180                        }
1181                    }
1182                    it.remove(); // expression simplified into primaryTypes set
1183                }
1184            }
1185            // readd the simplified primary types constraints
1186            if (primaryTypes != null) {
1187                if (primaryTypes.isEmpty()) {
1188                    // TODO better removal
1189                    primaryTypes.add("__NOSUCHTYPE__");
1190                }
1191                Expression expr;
1192                if (primaryTypes.size() == 1) {
1193                    String pt = primaryTypes.iterator().next();
1194                    expr = new Expression(new Reference(NXQL.ECM_PRIMARYTYPE), Operator.EQ, new StringLiteral(pt));
1195                } else { // primaryTypes.size() > 1
1196                    LiteralList list = new LiteralList();
1197                    for (String pt : primaryTypes) {
1198                        list.add(new StringLiteral(pt));
1199                    }
1200                    expr = new Expression(new Reference(NXQL.ECM_PRIMARYTYPE), Operator.IN, list);
1201                }
1202                toplevelOperands.addFirst(expr);
1203            }
1204        }
1205
1206        protected void analyzeToplevelIsProxy(Expression expr) {
1207            if (!(expr.rvalue instanceof IntegerLiteral)) {
1208                throw new QueryParseException(NXQL.ECM_ISPROXY + " requires literal 0 or 1 as right argument");
1209            }
1210            long v = ((IntegerLiteral) expr.rvalue).value;
1211            if (v != 0 && v != 1) {
1212                throw new QueryParseException(NXQL.ECM_ISPROXY + " requires literal 0 or 1 as right argument");
1213            }
1214            boolean isEq = expr.operator == Operator.EQ;
1215            updateProxyClause(Boolean.valueOf((v == 1) == isEq), expr);
1216        }
1217
1218        protected void analyzeToplevelProxyProperty(Expression expr) {
1219            // proxies required
1220            updateProxyClause(Boolean.TRUE, expr);
1221        }
1222
1223        private void updateProxyClause(Boolean value, Expression expr) {
1224            if (proxyClause != null && proxyClause != value) {
1225                throw new QueryCannotMatchException();
1226            }
1227            proxyClause = value;
1228            proxyClauseReason = expr.toString();
1229        }
1230
1231        @Override
1232        public void visitExpression(Expression node) {
1233            Reference ref = node.lvalue instanceof Reference ? (Reference) node.lvalue : null;
1234            String name = ref != null ? ref.name : null;
1235            if (name != null && name.startsWith(NXQL.ECM_FULLTEXT) && !NXQL.ECM_FULLTEXT_JOBID.equals(name)) {
1236                visitExpressionFulltext(node, name);
1237            } else {
1238                super.visitExpression(node);
1239            }
1240        }
1241
1242        protected void visitExpressionFulltext(Expression node, String name) {
1243            if (node.operator != Operator.EQ && node.operator != Operator.LIKE) {
1244                throw new QueryParseException(NXQL.ECM_FULLTEXT + " requires = or LIKE operator");
1245            }
1246            if (!(node.rvalue instanceof StringLiteral)) {
1247                throw new QueryParseException(NXQL.ECM_FULLTEXT + " requires literal string as right argument");
1248            }
1249            if (model.getRepositoryDescriptor().getFulltextDescriptor().getFulltextSearchDisabled()) {
1250                throw new QueryParseException("Fulltext search disabled by configuration");
1251            }
1252            String[] nameref = new String[] { name };
1253            boolean useIndex = findFulltextIndexOrField(model, nameref);
1254            if (useIndex) {
1255                ftCount++;
1256            }
1257        }
1258
1259        @Override
1260        public void visitReference(Reference node) {
1261            boolean hasTag = false;
1262            if (node.cast != null) {
1263                if (!DATE_CAST.equals(node.cast)) {
1264                    throw new QueryParseException("Invalid cast: " + node);
1265                }
1266            }
1267            String name = node.name;
1268            if (NXQL.ECM_PATH.equals(name) || //
1269                    NXQL.ECM_ANCESTORID.equals(name) || //
1270                    NXQL.ECM_ISPROXY.equals(name) || //
1271                    NXQL.ECM_MIXINTYPE.equals(name)) {
1272                if (inSelect) {
1273                    throw new QueryParseException("Cannot select on column: " + name);
1274                }
1275                if (inOrderBy) {
1276                    throw new QueryParseException("Cannot order by column: " + name);
1277                }
1278            } else if (NXQL.ECM_PRIMARYTYPE.equals(name) || //
1279                    NXQL.ECM_UUID.equals(name) || //
1280                    NXQL.ECM_NAME.equals(name) || //
1281                    NXQL.ECM_POS.equals(name) || //
1282                    NXQL.ECM_PARENTID.equals(name) || //
1283                    NXQL.ECM_LIFECYCLESTATE.equals(name) || //
1284                    NXQL.ECM_VERSIONLABEL.equals(name) || //
1285                    NXQL.ECM_VERSIONDESCRIPTION.equals(name) || //
1286                    NXQL.ECM_VERSIONCREATED.equals(name) || //
1287                    NXQL.ECM_VERSION_VERSIONABLEID.equals(name) || //
1288                    NXQL.ECM_ISLATESTVERSION.equals(name) || //
1289                    NXQL.ECM_ISLATESTMAJORVERSION.equals(name) || //
1290                    NXQL.ECM_ISVERSION_OLD.equals(name) || //
1291                    NXQL.ECM_ISVERSION.equals(name) || //
1292                    NXQL.ECM_ISCHECKEDIN.equals(name) || //
1293                    NXQL.ECM_LOCK.equals(name) || //
1294                    NXQL.ECM_LOCK_OWNER.equals(name) || //
1295                    NXQL.ECM_LOCK_CREATED.equals(name) || //
1296                    NXQL.ECM_PROXY_TARGETID.equals(name) || //
1297                    NXQL.ECM_PROXY_VERSIONABLEID.equals(name) || //
1298                    NXQL.ECM_FULLTEXT_JOBID.equals(name)) {
1299                // ok
1300            } else if (NXQL.ECM_TAG.equals(name) || name.startsWith(ECM_TAG_STAR)) {
1301                hasTag = true;
1302            } else if (NXQL.ECM_FULLTEXT_SCORE.equals(name)) {
1303                if (inOrderBy) {
1304                    orderByScore = true;
1305                } else if (inSelect) {
1306                    selectScore = true;
1307                } else {
1308                    throw new QueryParseException("Can only use column in SELECT or ORDER BY: " + name);
1309                }
1310            } else if (name.startsWith(NXQL.ECM_FULLTEXT)) {
1311                if (inSelect) {
1312                    throw new QueryParseException("Cannot select on column: " + name);
1313                }
1314                if (inOrderBy) {
1315                    throw new QueryParseException("Cannot order by column: " + name);
1316                }
1317                String[] nameref = new String[] { name };
1318                boolean useIndex = findFulltextIndexOrField(model, nameref);
1319                if (!useIndex) {
1320                    // LIKE on a field, continue analysing with that field
1321                    name = nameref[0];
1322                    checkProperty(name); // may throw
1323                }
1324                // else all is done in fulltext match info
1325            } else if (name.startsWith(NXQL.ECM_ACL)) {
1326                String simple = simpleXPath(name);
1327                if (simple.equals(ECM_SIMPLE_ACP_PRINCIPAL) || simple.equals(ECM_SIMPLE_ACP_PERMISSION)
1328                        || simple.equals(ECM_SIMPLE_ACP_GRANT) || simple.equals(ECM_SIMPLE_ACP_NAME)
1329                        || simple.equals(ECM_SIMPLE_ACP_POS) || simple.equals(ECM_SIMPLE_ACP_CREATOR)
1330                        || simple.equals(ECM_SIMPLE_ACP_BEGIN) || simple.equals(ECM_SIMPLE_ACP_END)
1331                        || simple.equals(ECM_SIMPLE_ACP_STATUS)) {
1332                    // ok
1333                } else {
1334                    throw new QueryParseException("Unknown field: " + name);
1335                }
1336            } else if (name.startsWith(NXQL.ECM_PREFIX)) {
1337                throw new QueryParseException("Unknown field: " + name);
1338            } else {
1339                checkProperty(name); // may throw
1340            }
1341
1342            if (inSelect) {
1343                whatColumnNames.add(name);
1344            } else if (inOrderBy) {
1345                orderByColumnNames.add(name);
1346            }
1347            if (hasWildcardIndex(name) || hasTag) {
1348                hasWildcardIndex = true;
1349                if (inOrderBy) {
1350                    orderByHasWildcardIndex = true;
1351                }
1352            }
1353        }
1354
1355        /**
1356         * Checks that a property exists.
1357         *
1358         * @throws QueryParseException if the property doesn't exist
1359         */
1360        protected void checkProperty(String xpath) {
1361            String simple = simpleXPath(xpath);
1362            ModelProperty prop = model.getPathPropertyInfo(simple);
1363            if (prop == null || prop.isIntermediateSegment()) {
1364                throw new QueryParseException("No such property: " + xpath);
1365            }
1366        }
1367
1368        @Override
1369        public void visitFunction(Function node) {
1370            if (!inSelect) {
1371                throw new QueryParseException("Function not supported in WHERE clause: " + node);
1372            }
1373            String func = node.name.toUpperCase();
1374            Operand arg;
1375            if (!AGGREGATE_FUNCTIONS.contains(func) || node.args.size() != 1
1376                    || !((arg = node.args.get(0)) instanceof Reference)) {
1377                throw new QueryParseException("Function not supported: " + node);
1378            }
1379            visitReference((Reference) arg);
1380        }
1381
1382        @Override
1383        public void visitOrderByClause(OrderByClause node) {
1384            inOrderBy = true;
1385            super.visitOrderByClause(node);
1386            inOrderBy = false;
1387        }
1388
1389    }
1390
1391    /**
1392     * Info about a column and its property type.
1393     */
1394    protected static class ColumnInfo {
1395
1396        public final Column column;
1397
1398        public final int arrayElementIndex;
1399
1400        public final boolean isArrayElement;
1401
1402        public final boolean needsSubSelect;
1403
1404        public ColumnInfo(Column column, int arrayElementIndex, boolean isArrayElement, boolean isArray) {
1405            this.column = column;
1406            this.arrayElementIndex = arrayElementIndex;
1407            this.isArrayElement = isArrayElement;
1408            this.needsSubSelect = !isArrayElement && isArray && !column.getType().isArray();
1409        }
1410    }
1411
1412    protected WhereBuilder newWhereBuilder(boolean isProxies) {
1413        return new WhereBuilder(isProxies);
1414    }
1415
1416    /**
1417     * Builds the database-level WHERE query from the AST.
1418     */
1419    protected class WhereBuilder extends DefaultQueryVisitor {
1420
1421        private static final long serialVersionUID = 1L;
1422
1423        public static final String PATH_SEP = "/";
1424
1425        public final LinkedList<Column> whatColumns = new LinkedList<Column>();
1426
1427        public final LinkedList<String> whatKeys = new LinkedList<String>();
1428
1429        public final StringBuilder buf = new StringBuilder();
1430
1431        // used to assign unique numbers to join aliases for complex property
1432        // wildcard indexes or tags
1433        protected int uniqueJoinIndex = 0;
1434
1435        protected int hierJoinCount = 0;
1436
1437        // path prefix -> hier table to join,
1438        protected Map<String, Table> propertyHierTables = new HashMap<String, Table>();
1439
1440        protected final boolean isProxies;
1441
1442        protected boolean aliasOrderByColumns;
1443
1444        // internal fields
1445
1446        protected boolean allowSubSelect;
1447
1448        protected boolean inSelect;
1449
1450        protected boolean inOrderBy;
1451
1452        protected int ftJoinNumber;
1453
1454        protected FulltextMatchInfo ftMatchInfo;
1455
1456        // true when visiting the rvalue of an id expression
1457        protected boolean visitingId;
1458
1459        // arrayColumnName or prefix/arrayColumnName -> array column subquery to join
1460        protected Map<String, ArraySubQuery> propertyArraySubQueries = new HashMap<String, ArraySubQuery>();
1461
1462        protected int arraySubQueryJoinCount = 0;
1463
1464        public WhereBuilder(boolean isProxies) {
1465            this.isProxies = isProxies;
1466        }
1467
1468        protected int getUniqueJoinIndex() {
1469            return ++uniqueJoinIndex;
1470        }
1471
1472        /**
1473         * Gets the arraySubquery for the given arrayColumn in the given contextKey, and maybe adds a JOIN if one is not
1474         * already done.
1475         * <p>
1476         * LEFT JOIN (SELECT id, UNNEST(somecol) AS item, generate_subscripts(somecol, 1) AS pos FROM someschema) _A1 ON
1477         * _A1.id = hierarchy.id
1478         */
1479        protected ArraySubQuery getArraySubQuery(Table contextHier, String contextKey, Column arrayColumn,
1480                boolean skipJoin) {
1481            ArraySubQuery arraySubQuery = propertyArraySubQueries.get(contextKey);
1482            if (arraySubQuery == null) {
1483                String alias = SUBQUERY_ARRAY_ALIAS + ++arraySubQueryJoinCount;
1484                arraySubQuery = dialect.getArraySubQuery(arrayColumn, alias);
1485                propertyArraySubQueries.put(contextKey, arraySubQuery);
1486                if (!skipJoin) {
1487                    Join join = new Join(Join.LEFT, arraySubQuery.toSql(), alias, null,
1488                            arraySubQuery.getSubQueryIdColumn().getFullQuotedName(),
1489                            contextHier.getColumn(Model.MAIN_KEY).getFullQuotedName());
1490                    joins.add(join);
1491                }
1492            }
1493            return arraySubQuery;
1494        }
1495
1496        protected ColumnInfo getSpecialColumnInfo(String name) {
1497            String propertyName = null;
1498            Table table = null;
1499            String fragmentKey = null;
1500            if (NXQL.ECM_UUID.equals(name)) {
1501                table = hierTable;
1502                fragmentKey = model.MAIN_KEY;
1503            } else if (NXQL.ECM_NAME.equals(name)) {
1504                table = hierTable;
1505                fragmentKey = model.HIER_CHILD_NAME_KEY;
1506            } else if (NXQL.ECM_POS.equals(name)) {
1507                table = hierTable;
1508                fragmentKey = model.HIER_CHILD_POS_KEY;
1509            } else if (NXQL.ECM_PARENTID.equals(name)) {
1510                table = hierTable;
1511                fragmentKey = model.HIER_PARENT_KEY;
1512            } else if (NXQL.ECM_ISVERSION_OLD.equals(name) || NXQL.ECM_ISVERSION.equals(name)) {
1513                table = hierTable;
1514                fragmentKey = model.MAIN_IS_VERSION_KEY;
1515            } else if (NXQL.ECM_ISCHECKEDIN.equals(name)) {
1516                table = hierTable;
1517                fragmentKey = model.MAIN_CHECKED_IN_KEY;
1518            } else if (NXQL.ECM_PRIMARYTYPE.equals(name)) {
1519                table = dataHierTable;
1520                fragmentKey = model.MAIN_PRIMARY_TYPE_KEY;
1521            } else if (NXQL.ECM_MIXINTYPE.equals(name)) {
1522                // toplevel ones have been extracted by the analyzer
1523                throw new QueryParseException("Cannot use non-toplevel " + name + " in query");
1524            } else if (NXQL.ECM_LIFECYCLESTATE.equals(name)) {
1525                propertyName = model.MISC_LIFECYCLE_STATE_PROP;
1526            } else if (NXQL.ECM_VERSIONLABEL.equals(name)) {
1527                propertyName = model.VERSION_LABEL_PROP;
1528            } else if (NXQL.ECM_VERSIONDESCRIPTION.equals(name)) {
1529                propertyName = model.VERSION_DESCRIPTION_PROP;
1530            } else if (NXQL.ECM_VERSIONCREATED.equals(name)) {
1531                propertyName = model.VERSION_CREATED_PROP;
1532            } else if (NXQL.ECM_VERSION_VERSIONABLEID.equals(name)) {
1533                propertyName = model.VERSION_VERSIONABLE_PROP;
1534            } else if (NXQL.ECM_ISLATESTVERSION.equals(name)) {
1535                propertyName = model.VERSION_IS_LATEST_PROP;
1536            } else if (NXQL.ECM_ISLATESTMAJORVERSION.equals(name)) {
1537                propertyName = model.VERSION_IS_LATEST_MAJOR_PROP;
1538            } else if (NXQL.ECM_LOCK.equals(name) || NXQL.ECM_LOCK_OWNER.equals(name)) {
1539                propertyName = model.LOCK_OWNER_PROP;
1540            } else if (NXQL.ECM_LOCK_CREATED.equals(name)) {
1541                propertyName = model.LOCK_CREATED_PROP;
1542            } else if (NXQL.ECM_PROXY_TARGETID.equals(name)) {
1543                table = proxyTable;
1544                fragmentKey = model.PROXY_TARGET_KEY;
1545            } else if (NXQL.ECM_PROXY_VERSIONABLEID.equals(name)) {
1546                table = proxyTable;
1547                fragmentKey = model.PROXY_VERSIONABLE_KEY;
1548            } else if (NXQL.ECM_FULLTEXT_JOBID.equals(name)) {
1549                propertyName = model.FULLTEXT_JOBID_PROP;
1550            } else if (NXQL.ECM_FULLTEXT_SCORE.equals(name)) {
1551                throw new QueryParseException(NXQL.ECM_FULLTEXT_SCORE + " cannot be used in WHERE clause");
1552            } else if (name.startsWith(NXQL.ECM_FULLTEXT)) {
1553                throw new QueryParseException(NXQL.ECM_FULLTEXT + " must be used as left-hand operand");
1554            } else if (NXQL.ECM_TAG.equals(name) || name.startsWith(ECM_TAG_STAR)) {
1555                /*
1556                 * JOIN relation _F1 ON hierarchy.id = _F1.source JOIN hierarchy _F2 ON _F1.id = _F2.id AND
1557                 * _F2.primarytype = 'Tagging' and returns _F2.name
1558                 */
1559                String suffix;
1560                if (name.startsWith(ECM_TAG_STAR)) {
1561                    suffix = name.substring(ECM_TAG_STAR.length());
1562                    if (suffix.isEmpty()) {
1563                        // any
1564                        suffix = "/*-" + getUniqueJoinIndex();
1565                    } else {
1566                        // named
1567                        suffix = "/*" + suffix;
1568                    }
1569                } else {
1570                    suffix = "";
1571                }
1572                String relContextKey = "_tag_relation" + suffix;
1573                Table rel = getFragmentTable(Join.INNER, dataHierTable, relContextKey, RELATION_TABLE, "source", -1,
1574                        false, null);
1575                String fragmentName = model.HIER_TABLE_NAME;
1576                fragmentKey = model.HIER_CHILD_NAME_KEY;
1577                String hierContextKey = "_tag_hierarchy" + suffix;
1578                table = getFragmentTable(Join.INNER, rel, hierContextKey, fragmentName, model.MAIN_KEY, -1, false,
1579                        TYPE_TAGGING);
1580            } else if (name.startsWith(NXQL.ECM_ACL)) {
1581                // get index and suffix; we already checked that there are two slashes
1582                int i = name.indexOf('/');
1583                int j = name.lastIndexOf('/');
1584                String index = name.substring(i + 1, j); // like "*1"
1585                String suffix = name.substring(j + 1); // like "principal"
1586                // re-create pseudo property name, which the Model mapped to a ModelProperty
1587                String newName = NXQL.ECM_ACL + '.' + suffix + '/' + index;
1588                return getRegularColumnInfo(newName);
1589            } else {
1590                throw new QueryParseException("No such property: " + name);
1591            }
1592            if (table == null) {
1593                ModelProperty propertyInfo = model.getPropertyInfo(propertyName);
1594                String fragmentName = propertyInfo.fragmentName;
1595                fragmentKey = propertyInfo.fragmentKey;
1596                if (fragmentName.equals(model.HIER_TABLE_NAME)) {
1597                    table = dataHierTable;
1598                } else {
1599                    table = getFragmentTable(dataHierTable, fragmentName, fragmentName, -1, false);
1600                }
1601            }
1602            Column column = table.getColumn(fragmentKey);
1603            return new ColumnInfo(column, -1, false, false);
1604        }
1605
1606        /**
1607         * Finds info about column (special or not).
1608         */
1609        public ColumnInfo getColumnInfo(String name) {
1610            if (name.startsWith(NXQL.ECM_PREFIX)) {
1611                return getSpecialColumnInfo(name);
1612            } else {
1613                return getRegularColumnInfo(name);
1614            }
1615        }
1616
1617        /**
1618         * Gets column information for a regular property.
1619         * <p>
1620         * Accumulates info about joins needed to get to this property.
1621         * <p>
1622         * IMPORTANT: THIS MUST NOT BE CALLED TWICE ON THE SAME PROPERTY as some structures are updated (joins,
1623         * counters).
1624         *
1625         * @throws QueryParseException if the property doesn't exist
1626         */
1627        protected ColumnInfo getRegularColumnInfo(String xpath) {
1628            Table contextHier;
1629            if (model.isProxySchemaPath(xpath)) {
1630                // if xpath for proxy, then change contextHier to proxyTable
1631                if (proxyTable != null) {
1632                    contextHier = hierTable;
1633                } else {
1634                    contextHier = dataHierTable;
1635                }
1636            } else {
1637                contextHier = dataHierTable;
1638            }
1639            xpath = canonicalXPath(xpath);
1640            String[] segments = xpath.split("/");
1641            String simple = null; // simplified prefix to match model
1642            String contextKey = null; // prefix used as key for table to join
1643            String segment = null;
1644            ModelProperty prop = null;
1645            for (int i = 0; i < segments.length; i++) {
1646                segment = segments[i];
1647                simple = simple == null ? segment : simple + '/' + segment;
1648                String contextStart = contextKey == null ? "" : contextKey + '/';
1649                String contextSuffix = "";
1650                int index = -1;
1651                boolean star = false;
1652                boolean isArrayElement = false;
1653                if (i < segments.length - 1) {
1654                    // check if we have a complex list index in the next
1655                    // position
1656                    String next = segments[i + 1];
1657                    if (INDEX.matcher(next).matches()) {
1658                        isArrayElement = true;
1659                        if (next.startsWith("*")) {
1660                            star = true;
1661                            next = next.substring(1);
1662                        }
1663                        if (!next.isEmpty()) {
1664                            index = Integer.parseInt(next);
1665                        }
1666                        // swallow next segment
1667                        i++;
1668                        simple += "/*";
1669                        if (star) {
1670                            if (index == -1) {
1671                                // any
1672                                contextSuffix = "/*-" + getUniqueJoinIndex();
1673                            } else {
1674                                // named
1675                                contextSuffix = "/*" + index;
1676                            }
1677                            index = -1;
1678                        } else {
1679                            contextSuffix = "/" + index;
1680                        }
1681                    }
1682                }
1683
1684                prop = model.getPathPropertyInfo(simple);
1685                if (prop == null) {
1686                    throw new QueryParseException("No such property: " + xpath);
1687                }
1688                if (i < segments.length - 1) {
1689                    // non-final segment
1690                    if (!prop.isIntermediateSegment()) {
1691                        throw new QueryParseException("No such property: " + xpath);
1692                    }
1693                    segment = prop.getIntermediateSegment(); // canonical
1694                    contextKey = contextStart + segment + contextSuffix;
1695                    Table table = propertyHierTables.get(contextKey);
1696                    if (table == null) {
1697                        // none existing
1698                        // create new Join with hierarchy from previous
1699                        String alias = TABLE_HIER_ALIAS + ++hierJoinCount;
1700                        table = new TableAlias(dataHierTable, alias);
1701                        propertyHierTables.put(contextKey, table);
1702                        addJoin(Join.LEFT, alias, table, model.HIER_PARENT_KEY, contextHier, model.MAIN_KEY, segment,
1703                                index, null);
1704                    }
1705                    contextHier = table;
1706                } else {
1707                    // last segment
1708                    if (prop.isIntermediateSegment()) {
1709                        throw new QueryParseException("No such property: " + xpath);
1710                    }
1711                    Table table = database.getTable(prop.fragmentName);
1712                    Column column = table.getColumn(prop.fragmentKey);
1713                    boolean skipJoin = !isArrayElement && prop.propertyType.isArray() && !column.isArray();
1714                    if (column.isArray() && star) {
1715                        contextKey = contextStart + segment + contextSuffix;
1716                        ArraySubQuery arraySubQuery = getArraySubQuery(contextHier, contextKey, column, skipJoin);
1717                        column = arraySubQuery.getSubQueryValueColumn();
1718                    } else {
1719                        // use fragment name, not segment, for table context key
1720                        contextKey = contextStart + prop.fragmentName + contextSuffix;
1721                        table = getFragmentTable(contextHier, contextKey, prop.fragmentName,
1722                                column.isArray() ? -1 : index, skipJoin);
1723                        column = table.getColumn(prop.fragmentKey);
1724                    }
1725                    return new ColumnInfo(column, column.isArray() ? index : -1, isArrayElement,
1726                            prop.propertyType.isArray());
1727                }
1728            }
1729            throw new AssertionError("not reached");
1730        }
1731
1732        @Override
1733        public void visitQuery(SQLQuery node) {
1734            super.visitQuery(node);
1735            // intentionally does not set limit or offset in the query
1736        }
1737
1738        @Override
1739        public void visitSelectClause(SelectClause node) {
1740            inSelect = true;
1741            super.visitSelectClause(node);
1742            inSelect = false;
1743        }
1744
1745        @Override
1746        public void visitMultiExpression(MultiExpression node) {
1747            buf.append('(');
1748            for (Iterator<Operand> it = node.values.iterator(); it.hasNext();) {
1749                it.next().accept(this);
1750                if (it.hasNext()) {
1751                    node.operator.accept(this);
1752                }
1753            }
1754            buf.append(')');
1755        }
1756
1757        @Override
1758        public void visitExpression(Expression node) {
1759            buf.append('(');
1760            Reference ref = node.lvalue instanceof Reference ? (Reference) node.lvalue : null;
1761            String name = ref != null ? ref.name : null;
1762            String cast = ref != null ? ref.cast : null;
1763            Operand rvalue = node.rvalue;
1764            if (DATE_CAST.equals(cast)) {
1765                checkDateLiteralForCast(rvalue, node);
1766            }
1767            Operator op = node.operator;
1768            if (op == Operator.STARTSWITH) {
1769                visitExpressionStartsWith(node);
1770            } else if (NXQL.ECM_PATH.equals(name)) {
1771                visitExpressionEcmPath(node);
1772            } else if (NXQL.ECM_ANCESTORID.equals(name)) {
1773                visitExpressionAncestorId(node);
1774            } else if (NXQL.ECM_ISPROXY.equals(name)) {
1775                visitExpressionIsProxy(node);
1776            } else if (NXQL.ECM_ISVERSION_OLD.equals(name) || NXQL.ECM_ISVERSION.equals(name)) {
1777                visitExpressionWhereFalseIsNull(node);
1778            } else if (NXQL.ECM_ISCHECKEDIN.equals(name) || NXQL.ECM_ISLATESTVERSION.equals(name)
1779                    || NXQL.ECM_ISLATESTMAJORVERSION.equals(name)) {
1780                visitExpressionWhereFalseMayBeNull(node);
1781            } else if (NXQL.ECM_MIXINTYPE.equals(name)) {
1782                visitExpressionMixinType(node);
1783            } else if (name != null && name.startsWith(NXQL.ECM_FULLTEXT) && !NXQL.ECM_FULLTEXT_JOBID.equals(name)) {
1784                visitExpressionFulltext(node, name);
1785            } else if ((op == Operator.EQ || op == Operator.NOTEQ || op == Operator.IN || op == Operator.NOTIN
1786                    || op == Operator.LIKE || op == Operator.NOTLIKE || op == Operator.ILIKE
1787                    || op == Operator.NOTILIKE)) {
1788                ColumnInfo info = name == null ? null : getColumnInfo(name);
1789                // node.lvalue must not be accepted from now on
1790                if (info != null && info.needsSubSelect) {
1791                    // use EXISTS with subselect clause
1792                    boolean direct = op == Operator.EQ || op == Operator.IN || op == Operator.LIKE
1793                            || op == Operator.ILIKE;
1794                    Operator directOp = direct ? op
1795                            : (op == Operator.NOTEQ ? Operator.EQ
1796                                    : op == Operator.NOTIN ? Operator.IN
1797                                            : op == Operator.NOTLIKE ? Operator.LIKE : Operator.ILIKE);
1798                    if (!direct) {
1799                        buf.append("NOT ");
1800                    }
1801                    generateExistsStart(buf, info.column.getTable());
1802                    allowSubSelect = true;
1803                    visitColumnExpression(info.column, directOp, rvalue, cast, name, info.arrayElementIndex);
1804                    allowSubSelect = false;
1805                    generateExistsEnd(buf);
1806                } else if (info != null) {
1807                    // boolean literals have to be translated according the
1808                    // database dialect
1809                    if (info.column.getType() == ColumnType.BOOLEAN) {
1810                        rvalue = getBooleanLiteral(rvalue);
1811                    }
1812                    visitColumnExpression(info.column, op, rvalue, cast, name, info.arrayElementIndex);
1813                } else {
1814                    super.visitExpression(node);
1815                }
1816            } else if (op == Operator.BETWEEN || op == Operator.NOTBETWEEN) {
1817                LiteralList l = (LiteralList) rvalue;
1818                if (DATE_CAST.equals(cast)) {
1819                    checkDateLiteralForCast(l.get(0), node);
1820                    checkDateLiteralForCast(l.get(1), node);
1821                }
1822                node.lvalue.accept(this);
1823                buf.append(' ');
1824                op.accept(this);
1825                buf.append(' ');
1826                l.get(0).accept(this);
1827                buf.append(" AND ");
1828                l.get(1).accept(this);
1829            } else {
1830                super.visitExpression(node);
1831            }
1832            buf.append(')');
1833        }
1834
1835        protected Operand getBooleanLiteral(Operand rvalue) {
1836            if (!(rvalue instanceof IntegerLiteral)) {
1837                throw new QueryParseException("Boolean expressions require literal 0 or 1 as right argument");
1838            }
1839            long v = ((IntegerLiteral) rvalue).value;
1840            if (v != 0 && v != 1) {
1841                throw new QueryParseException("Boolean expressions require literal 0 or 1 as right argument");
1842            }
1843            return new BooleanLiteral(v == 1);
1844        }
1845
1846        protected void visitColumnExpression(Column column, Operator op, Operand rvalue, String cast, String lvalueName,
1847                int arrayElementIndex) {
1848            if (op == Operator.EQ || op == Operator.NOTEQ || op == Operator.IN || op == Operator.NOTIN) {
1849                visitExpressionEqOrIn(column, op, rvalue, cast, arrayElementIndex);
1850            } else if (op == Operator.LIKE || op == Operator.NOTLIKE) {
1851                visitExpressionLike(column, op, rvalue, lvalueName, arrayElementIndex);
1852            } else if (op == Operator.ILIKE || op == Operator.NOTILIKE) {
1853                visitExpressionIlike(column, op, rvalue, lvalueName, arrayElementIndex);
1854            } else {
1855                visitSimpleExpression(column, op, rvalue, cast, -1);
1856            }
1857        }
1858
1859        protected void visitSimpleExpression(Column column, Operator op, Operand rvalue, String cast,
1860                int arrayElementIndex) {
1861            visitReference(column, cast, arrayElementIndex);
1862            op.accept(this);
1863            boolean oldVisitingId = visitingId;
1864            visitingId = column.getType().isId();
1865            rvalue.accept(this);
1866            visitingId = oldVisitingId;
1867        }
1868
1869        /**
1870         * This operand is going to be used with a lvalue that has a DATE cast, so if it's a date literal make sure it's
1871         * not a TIMESTAMP.
1872         */
1873        protected void checkDateLiteralForCast(Operand value, Expression node) {
1874            if (value instanceof DateLiteral && !((DateLiteral) value).onlyDate) {
1875                throw new QueryParseException("DATE() cast must be used with DATE literal, not TIMESTAMP: " + node);
1876            }
1877        }
1878
1879        protected void generateExistsStart(StringBuilder buf, Table table) {
1880            String tableName;
1881            if (table.isAlias()) {
1882                tableName = table.getRealTable().getQuotedName() + " " + table.getQuotedName();
1883            } else {
1884                tableName = table.getQuotedName();
1885            }
1886            buf.append(String.format("EXISTS (SELECT 1 FROM %s WHERE %s = %s AND ", tableName,
1887                    dataHierTable.getColumn(model.MAIN_KEY).getFullQuotedName(),
1888                    table.getColumn(model.MAIN_KEY).getFullQuotedName()));
1889        }
1890
1891        protected void generateExistsEnd(StringBuilder buf) {
1892            buf.append(")");
1893        }
1894
1895        protected void visitExpressionStartsWith(Expression node) {
1896            if (!(node.lvalue instanceof Reference)) {
1897                throw new QueryParseException("Illegal left argument for " + Operator.STARTSWITH + ": " + node.lvalue);
1898            }
1899            if (!(node.rvalue instanceof StringLiteral)) {
1900                throw new QueryParseException(Operator.STARTSWITH + " requires literal path as right argument");
1901            }
1902            String path = ((StringLiteral) node.rvalue).value;
1903            if (path.length() > 1 && path.endsWith(PATH_SEP)) {
1904                path = path.substring(0, path.length() - PATH_SEP.length());
1905            }
1906            String name = ((Reference) node.lvalue).name;
1907            if (NXQL.ECM_PATH.equals(name)) {
1908                visitExpressionStartsWithPath(path);
1909            } else {
1910                visitExpressionStartsWithNonPath(node, path);
1911            }
1912        }
1913
1914        protected void visitExpressionStartsWithPath(String path) {
1915            // find the id from the path
1916            Serializable id = pathResolver.getIdForPath(path);
1917            if (id == null) {
1918                // no such path, always return a false
1919                // TODO remove the expression more intelligently from the parse
1920                // tree
1921                buf.append("0=1");
1922            } else {
1923                // id is always valid, no need to pass it as argument to getInTreeSql
1924                buf.append(dialect.getInTreeSql(hierTable.getColumn(model.MAIN_KEY).getFullQuotedName(), null));
1925                whereParams.add(id);
1926            }
1927        }
1928
1929        protected void visitExpressionStartsWithNonPath(Expression node, String path) {
1930            String name = ((Reference) node.lvalue).name;
1931            ColumnInfo info = getColumnInfo(name);
1932            if (info.needsSubSelect) {
1933                // use EXISTS with subselect clause
1934                generateExistsStart(buf, info.column.getTable());
1935            }
1936            buf.append('(');
1937            visitExpressionEqOrIn(info.column, Operator.EQ, new StringLiteral(path), null, -1);
1938            visitOperator(Operator.OR);
1939            // TODO escape % chars...
1940            visitExpressionLike(info.column, Operator.LIKE, new StringLiteral(path + PATH_SEP + '%'), name, -1);
1941            buf.append(')');
1942            if (info.needsSubSelect) {
1943                generateExistsEnd(buf);
1944            }
1945        }
1946
1947        protected void visitExpressionEcmPath(Expression node) {
1948            if (node.operator != Operator.EQ && node.operator != Operator.NOTEQ) {
1949                throw new QueryParseException(NXQL.ECM_PATH + " requires = or <> operator");
1950            }
1951            if (!(node.rvalue instanceof StringLiteral)) {
1952                throw new QueryParseException(NXQL.ECM_PATH + " requires literal path as right argument");
1953            }
1954            String path = ((StringLiteral) node.rvalue).value;
1955            if (path.length() > 1 && path.endsWith(PATH_SEP)) {
1956                path = path.substring(0, path.length() - PATH_SEP.length());
1957            }
1958            Serializable id = pathResolver.getIdForPath(path);
1959            if (id == null) {
1960                // no such path, always return a false
1961                // TODO remove the expression more intelligently from the parse
1962                // tree
1963                buf.append("0=1");
1964            } else {
1965                visitReference(hierTable.getColumn(model.MAIN_KEY));
1966                visitOperator(node.operator);
1967                visitId(model.idToString(id));
1968            }
1969        }
1970
1971        protected void visitExpressionAncestorId(Expression node) {
1972            if (node.operator != Operator.EQ && node.operator != Operator.NOTEQ) {
1973                throw new QueryParseException(NXQL.ECM_ANCESTORID + " requires = or <> operator");
1974            }
1975            if (!(node.rvalue instanceof StringLiteral)) {
1976                throw new QueryParseException(NXQL.ECM_ANCESTORID + " requires literal id as right argument");
1977            }
1978            boolean not = node.operator == Operator.NOTEQ;
1979            String id = ((StringLiteral) node.rvalue).value;
1980            if (not) {
1981                buf.append("(NOT (");
1982            }
1983            String sql = dialect.getInTreeSql(hierTable.getColumn(model.MAIN_KEY).getFullQuotedName(), id);
1984            if (sql == null) {
1985                buf.append("0=1");
1986            } else {
1987                buf.append(sql);
1988                whereParams.add(id);
1989            }
1990            if (not) {
1991                buf.append("))");
1992            }
1993        }
1994
1995        protected void visitExpressionIsProxy(Expression node) {
1996            boolean bool = getBooleanRValue(NXQL.ECM_ISPROXY, node);
1997            buf.append(isProxies == bool ? "1=1" : "0=1");
1998        }
1999
2000        protected void visitExpressionWhereFalseIsNull(Expression node) {
2001            String name = ((Reference) node.lvalue).name;
2002            boolean bool = getBooleanRValue(name, node);
2003            node.lvalue.accept(this);
2004            if (bool) {
2005                buf.append(" = ");
2006                buf.append(dialect.toBooleanValueString(true));
2007            } else {
2008                buf.append(" IS NULL");
2009            }
2010        }
2011
2012        protected void visitExpressionWhereFalseMayBeNull(Expression node) {
2013            String name = ((Reference) node.lvalue).name;
2014            boolean bool = getBooleanRValue(name, node);
2015            if (bool) {
2016                node.lvalue.accept(this);
2017                buf.append(" = ");
2018                buf.append(dialect.toBooleanValueString(true));
2019            } else {
2020                buf.append('(');
2021                node.lvalue.accept(this);
2022                buf.append(" = ");
2023                buf.append(dialect.toBooleanValueString(false));
2024                buf.append(" OR ");
2025                node.lvalue.accept(this);
2026                buf.append(" IS NULL)");
2027            }
2028        }
2029
2030        private boolean getBooleanRValue(String name, Expression node) {
2031            if (node.operator != Operator.EQ && node.operator != Operator.NOTEQ) {
2032                throw new QueryParseException(name + " requires = or <> operator");
2033            }
2034            long v;
2035            if (!(node.rvalue instanceof IntegerLiteral)
2036                    || ((v = ((IntegerLiteral) node.rvalue).value) != 0 && v != 1)) {
2037                throw new QueryParseException(name + " requires literal 0 or 1 as right argument");
2038            }
2039            boolean bool = node.operator == Operator.EQ ^ v == 0;
2040            return bool;
2041        }
2042
2043        /**
2044         * Include or exclude mixins.
2045         * <p>
2046         * include: primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%'
2047         * <p>
2048         * exclude: primarytype IN (... types without Foo or Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes
2049         * NOT LIKE '%Bar%' OR mixintypes IS NULL)
2050         */
2051        protected void visitExpressionMixinType(Expression node) {
2052            boolean include;
2053            Set<String> mixins;
2054
2055            Expression expr = (Expression) node;
2056            Operator op = expr.operator;
2057            if (op == Operator.EQ || op == Operator.NOTEQ) {
2058                include = op == Operator.EQ;
2059                if (!(expr.rvalue instanceof StringLiteral)) {
2060                    throw new QueryParseException(NXQL.ECM_MIXINTYPE + " = requires literal string as right argument");
2061                }
2062                String value = ((StringLiteral) expr.rvalue).value;
2063                mixins = Collections.singleton(value);
2064            } else if (op == Operator.IN || op == Operator.NOTIN) {
2065                include = op == Operator.IN;
2066                if (!(expr.rvalue instanceof LiteralList)) {
2067                    throw new QueryParseException(NXQL.ECM_MIXINTYPE + " = requires string list as right argument");
2068                }
2069                mixins = getStringLiterals((LiteralList) expr.rvalue);
2070            } else {
2071                throw new QueryParseException(NXQL.ECM_MIXINTYPE + " unknown operator: " + op);
2072            }
2073
2074            /*
2075             * Primary types
2076             */
2077
2078            Set<String> types;
2079            if (include) {
2080                types = new HashSet<String>();
2081                for (String mixin : mixins) {
2082                    types.addAll(model.getMixinDocumentTypes(mixin));
2083                }
2084            } else {
2085                types = new HashSet<String>(model.getDocumentTypes());
2086                for (String mixin : mixins) {
2087                    types.removeAll(model.getMixinDocumentTypes(mixin));
2088                }
2089            }
2090
2091            /*
2092             * Instance mixins
2093             */
2094
2095            Set<String> instanceMixins = new HashSet<String>();
2096            for (String mixin : mixins) {
2097                if (!neverPerInstanceMixins.contains(mixin)) {
2098                    instanceMixins.add(mixin);
2099                }
2100            }
2101
2102            /*
2103             * SQL generation
2104             */
2105
2106            if (!types.isEmpty()) {
2107                Column col = dataHierTable.getColumn(model.MAIN_PRIMARY_TYPE_KEY);
2108                visitReference(col);
2109                buf.append(" IN ");
2110                buf.append('(');
2111                for (Iterator<String> it = types.iterator(); it.hasNext();) {
2112                    visitStringLiteral(it.next());
2113                    if (it.hasNext()) {
2114                        buf.append(", ");
2115                    }
2116                }
2117                buf.append(')');
2118
2119                if (!instanceMixins.isEmpty()) {
2120                    buf.append(include ? " OR " : " AND ");
2121                }
2122            }
2123
2124            if (!instanceMixins.isEmpty()) {
2125                buf.append('(');
2126                Column mixinsColumn = dataHierTable.getColumn(model.MAIN_MIXIN_TYPES_KEY);
2127                String[] returnParam = new String[1];
2128                for (Iterator<String> it = instanceMixins.iterator(); it.hasNext();) {
2129                    String mixin = it.next();
2130                    String sql = dialect.getMatchMixinType(mixinsColumn, mixin, include, returnParam);
2131                    buf.append(sql);
2132                    if (returnParam[0] != null) {
2133                        whereParams.add(returnParam[0]);
2134                    }
2135                    if (it.hasNext()) {
2136                        buf.append(include ? " OR " : " AND ");
2137                    }
2138                }
2139                if (!include) {
2140                    buf.append(" OR ");
2141                    visitReference(mixinsColumn);
2142                    buf.append(" IS NULL");
2143                }
2144                buf.append(')');
2145            }
2146
2147            if (types.isEmpty() && instanceMixins.isEmpty()) {
2148                buf.append(include ? "0=1" : "0=0");
2149            }
2150        }
2151
2152        protected void visitExpressionFulltext(Expression node, String name) {
2153            String[] nameref = new String[] { name };
2154            boolean useIndex = findFulltextIndexOrField(model, nameref);
2155            name = nameref[0];
2156            if (useIndex) {
2157                // use actual fulltext query using a dedicated index
2158                String fulltextQuery = ((StringLiteral) node.rvalue).value;
2159                fulltextQuery = dialect.getDialectFulltextQuery(fulltextQuery);
2160                ftJoinNumber++;
2161                Column mainColumn = dataHierTable.getColumn(model.MAIN_KEY);
2162                FulltextMatchInfo info = dialect.getFulltextScoredMatchInfo(fulltextQuery, name, ftJoinNumber,
2163                        mainColumn, model, database);
2164                ftMatchInfo = info;
2165                if (info.joins != null) {
2166                    joins.addAll(info.joins);
2167                }
2168                buf.append(info.whereExpr);
2169                if (info.whereExprParam != null) {
2170                    whereParams.add(info.whereExprParam);
2171                }
2172            } else {
2173                // single field matched with ILIKE
2174                log.warn("No fulltext index configured for field " + name + ", falling back on LIKE query");
2175                String value = ((StringLiteral) node.rvalue).value;
2176
2177                // fulltext translation into pseudo-LIKE syntax
2178                Set<String> words = FullTextUtils.parseFullText(value, false);
2179                if (words.isEmpty()) {
2180                    // only stop words or empty
2181                    value = "DONTMATCHANYTHINGFOREMPTYQUERY";
2182                } else {
2183                    value = "%" + StringUtils.join(new ArrayList<String>(words), "%") + "%";
2184                }
2185
2186                Reference ref = new Reference(name);
2187                if (dialect.supportsIlike()) {
2188                    visitReference(ref);
2189                    buf.append(" ILIKE ");
2190                    visitStringLiteral(value);
2191                } else {
2192                    buf.append("LOWER(");
2193                    visitReference(ref);
2194                    buf.append(") LIKE ");
2195                    visitStringLiteral(value);
2196                }
2197            }
2198        }
2199
2200        protected void visitExpressionEqOrIn(Column column, Operator op, Operand rvalue, String cast,
2201                int arrayElementIndex) {
2202            if (column.isArray() && arrayElementIndex == -1) {
2203                List<Serializable> params;
2204                if (rvalue instanceof Literal) {
2205                    Serializable param = getSerializableLiteral((Literal) rvalue);
2206                    params = Collections.singletonList(param);
2207                } else {
2208                    params = getSerializableLiterals((LiteralList) rvalue);
2209                }
2210                boolean positive = op == Operator.EQ || op == Operator.IN;
2211                String sql = dialect.getArrayInSql(column, cast, positive, params);
2212                buf.append(sql);
2213                whereParams.addAll(params);
2214            } else {
2215                visitSimpleExpression(column, op, rvalue, cast, arrayElementIndex);
2216            }
2217        }
2218
2219        protected void visitExpressionLike(Column column, Operator op, Operand rvalue, String lvalueName,
2220                int arrayElementIndex) {
2221            if (column.isArray() && arrayElementIndex == -1) {
2222                if (lvalueName == null) {
2223                    throw new AssertionError("Name is required when lvalue is an array");
2224                }
2225                boolean positive = (op == Operator.LIKE);
2226                String sql = dialect.getArrayLikeSql(column, lvalueName, positive, dataHierTable);
2227                buf.append(sql);
2228                whereParams.add(getSerializableLiteral((Literal) rvalue));
2229            } else {
2230                visitSimpleExpression(column, op, rvalue, null, arrayElementIndex);
2231                addLikeEscaping();
2232            }
2233        }
2234
2235        protected void visitExpressionIlike(Column column, Operator op, Operand rvalue, String lvalueName,
2236                int arrayElementIndex) {
2237            if (column.isArray() && arrayElementIndex == -1) {
2238                if (lvalueName == null) {
2239                    throw new AssertionError("Name is required when lvalue is an array");
2240                }
2241                boolean positive = op == Operator.ILIKE;
2242                String sql = dialect.getArrayIlikeSql(column, lvalueName, positive, dataHierTable);
2243                buf.append(sql);
2244                whereParams.add(getSerializableLiteral((Literal) rvalue));
2245            } else if (dialect.supportsIlike()) {
2246                visitSimpleExpression(column, op, rvalue, null, arrayElementIndex);
2247            } else {
2248                buf.append("LOWER(");
2249                visitReference(column, arrayElementIndex);
2250                buf.append(") ");
2251                if (op == Operator.NOTILIKE) {
2252                    buf.append("NOT ");
2253                }
2254                buf.append("LIKE");
2255                buf.append(" LOWER(");
2256                rvalue.accept(this);
2257                buf.append(")");
2258                addLikeEscaping();
2259            }
2260        }
2261
2262        protected void addLikeEscaping() {
2263            String escape = dialect.getLikeEscaping();
2264            if (escape != null) {
2265                buf.append(escape);
2266            }
2267        }
2268
2269        @Override
2270        public void visitOperator(Operator node) {
2271            if (node != Operator.NOT) {
2272                buf.append(' ');
2273            }
2274            buf.append(node.toString());
2275            buf.append(' ');
2276        }
2277
2278        @Override
2279        public void visitReference(Reference node) {
2280            String name = node.name;
2281            if (NXQL.ECM_FULLTEXT_SCORE.equals(name)) {
2282                visitScore();
2283                return;
2284            }
2285            ColumnInfo info = getColumnInfo(name);
2286            if (info.needsSubSelect && !allowSubSelect) {
2287                String msg = inOrderBy ? "Cannot use collection %s in ORDER BY clause"
2288                        : "Can only use collection %s with =, <>, IN or NOT IN clause";
2289                throw new QueryParseException(String.format(msg, name));
2290            }
2291            if (inSelect) {
2292                whatColumns.add(info.column);
2293                whatKeys.add(name);
2294            } else {
2295                visitReference(info.column, node.cast);
2296            }
2297        }
2298
2299        protected void visitReference(Column column) {
2300            visitReference(column, null, -1);
2301        }
2302
2303        protected void visitReference(Column column, String cast) {
2304            visitReference(column, cast, -1);
2305        }
2306
2307        protected void visitReference(Column column, int arrayElementIndex) {
2308            visitReference(column, null, arrayElementIndex);
2309        }
2310
2311        protected void visitReference(Column column, String cast, int arrayElementIndex) {
2312            if (DATE_CAST.equals(cast) && column.getType() != ColumnType.TIMESTAMP) {
2313                throw new QueryParseException("Cannot cast to " + cast + ": " + column);
2314            }
2315            String qname = column.getFullQuotedName();
2316            if (arrayElementIndex != -1) {
2317                if (column.isArray()) {
2318                    qname = dialect.getArrayElementString(qname, arrayElementIndex);
2319                } else {
2320                    throw new QueryParseException(
2321                            "Cannot use array index " + arrayElementIndex + " for non-array column " + column);
2322                }
2323            }
2324            // some databases (Derby) can't do comparisons on CLOB
2325            if (column.getJdbcType() == Types.CLOB) {
2326                String colFmt = dialect.getClobCast(inOrderBy);
2327                if (colFmt != null) {
2328                    qname = String.format(colFmt, qname, Integer.valueOf(255));
2329                }
2330            }
2331            if (cast != null) {
2332                // only DATE cast for now
2333                String fmt = dialect.getDateCast();
2334                buf.append(String.format(fmt, qname));
2335            } else {
2336                buf.append(qname);
2337            }
2338        }
2339
2340        @Override
2341        public void visitLiteralList(LiteralList node) {
2342            buf.append('(');
2343            for (Iterator<Literal> it = node.iterator(); it.hasNext();) {
2344                it.next().accept(this);
2345                if (it.hasNext()) {
2346                    buf.append(", ");
2347                }
2348            }
2349            buf.append(')');
2350        }
2351
2352        @Override
2353        public void visitDateLiteral(DateLiteral node) {
2354            buf.append('?');
2355            if (node.onlyDate) {
2356                whereParams.add(node.toSqlDate());
2357            } else {
2358                whereParams.add(node.toCalendar());
2359            }
2360        }
2361
2362        @Override
2363        public void visitStringLiteral(StringLiteral node) {
2364            if (visitingId) {
2365                visitId(node.value);
2366            } else {
2367                visitStringLiteral(node.value);
2368            }
2369        }
2370
2371        // wrap the string so that the mapper can detect it
2372        // and map to an actual database id
2373        protected void visitId(String string) {
2374            buf.append('?');
2375            whereParams.add(new WrappedId(string));
2376        }
2377
2378        public void visitStringLiteral(String string) {
2379            buf.append('?');
2380            whereParams.add(string);
2381        }
2382
2383        @Override
2384        public void visitDoubleLiteral(DoubleLiteral node) {
2385            buf.append(node.value);
2386        }
2387
2388        @Override
2389        public void visitIntegerLiteral(IntegerLiteral node) {
2390            buf.append(node.value);
2391        }
2392
2393        @Override
2394        public void visitBooleanLiteral(BooleanLiteral node) {
2395            buf.append('?');
2396            whereParams.add(Boolean.valueOf(node.value));
2397        }
2398
2399        @Override
2400        public void visitFunction(Function node) {
2401            String func = node.name.toUpperCase();
2402            Reference ref = (Reference) node.args.get(0);
2403            ref.accept(this); // whatColumns / whatKeys for column
2404
2405            // replace column info with aggregate
2406            Column col = whatColumns.removeLast();
2407            String key = whatKeys.removeLast();
2408            final String aggFQN = func + "(" + col.getFullQuotedName() + ")";
2409            final ColumnType aggType = getAggregateType(func, col.getType());
2410            final int aggJdbcType = dialect.getJDBCTypeAndString(aggType).jdbcType;
2411            Column cc = new Column(col, col.getTable()) {
2412                private static final long serialVersionUID = 1L;
2413
2414                @Override
2415                public String getFullQuotedName() {
2416                    return aggFQN;
2417                }
2418
2419                @Override
2420                public ColumnType getType() {
2421                    return aggType;
2422                }
2423
2424                @Override
2425                public int getJdbcType() {
2426                    return aggJdbcType;
2427                }
2428            };
2429            whatColumns.add(cc);
2430            whatKeys.add(func + "(" + key + ")");
2431        }
2432
2433        protected void visitScore() {
2434            if (inSelect) {
2435                Column col = new Column(hierTable, null, ColumnType.DOUBLE, null);
2436                whatColumns.add(col);
2437                whatKeys.add(NXQL.ECM_FULLTEXT_SCORE);
2438            } else {
2439                buf.append(aliasesByName.get(NXQL.ECM_FULLTEXT_SCORE));
2440            }
2441        }
2442
2443        protected ColumnType getAggregateType(String func, ColumnType arg) {
2444            if (COUNT_FUNCTION.equals(func)) {
2445                return ColumnType.LONG;
2446            }
2447            if (AVG_FUNCTION.equals(func)) {
2448                return ColumnType.DOUBLE;
2449            }
2450            // SUM, MIN, MAX
2451            return arg;
2452        }
2453
2454        @Override
2455        public void visitOrderByList(OrderByList node) {
2456            inOrderBy = true;
2457            for (Iterator<OrderByExpr> it = node.iterator(); it.hasNext();) {
2458                it.next().accept(this);
2459                if (it.hasNext()) {
2460                    buf.append(", ");
2461                }
2462            }
2463            inOrderBy = false;
2464        }
2465
2466        @Override
2467        public void visitOrderByExpr(OrderByExpr node) {
2468            int length = buf.length();
2469            // generates needed joins
2470            super.visitOrderByExpr(node); // visit reference
2471            if (aliasOrderByColumns) {
2472                // but don't use generated values
2473                // make the ORDER BY clause uses the aliases instead
2474                buf.setLength(length);
2475                buf.append(aliasesByName.get(node.reference.name));
2476            }
2477            if (node.isDescending) {
2478                buf.append(dialect.getDescending());
2479            }
2480        }
2481
2482    }
2483
2484}