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