/* * (C) Copyright 2006-2017 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.opencmis.impl.server; import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; import java.security.Principal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.tree.Tree; import org.apache.chemistry.opencmis.commons.PropertyIds; import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition; import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition; import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer; import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; import org.apache.chemistry.opencmis.commons.enums.Cardinality; import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalDefinitionImpl; import org.apache.chemistry.opencmis.server.support.query.AbstractPredicateWalker; import org.apache.chemistry.opencmis.server.support.query.CmisQlStrictLexer; import org.apache.chemistry.opencmis.server.support.query.CmisSelector; import org.apache.chemistry.opencmis.server.support.query.ColumnReference; import org.apache.chemistry.opencmis.server.support.query.FunctionReference; import org.apache.chemistry.opencmis.server.support.query.FunctionReference.CmisQlFunction; import org.apache.chemistry.opencmis.server.support.query.QueryObject; import org.apache.chemistry.opencmis.server.support.query.QueryUtilStrict; import org.apache.chemistry.opencmis.server.support.query.QueryObject.JoinSpec; import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.LifeCycleConstants; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl; import org.nuxeo.ecm.core.query.QueryFilter; import org.nuxeo.ecm.core.query.QueryParseException; import org.nuxeo.ecm.core.schema.FacetNames; import org.nuxeo.ecm.core.security.SecurityPolicy; import org.nuxeo.ecm.core.security.SecurityPolicy.QueryTransformer; import org.nuxeo.ecm.core.security.SecurityPolicyService; import org.nuxeo.ecm.core.storage.sql.Model; import org.nuxeo.ecm.core.storage.sql.ModelProperty; import org.nuxeo.ecm.core.storage.sql.Session.PathResolver; import org.nuxeo.ecm.core.storage.sql.jdbc.QueryMaker; import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo; import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.MapMaker; import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.SQLInfoSelect; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; import org.nuxeo.ecm.core.storage.sql.jdbc.db.TableAlias; import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect.FulltextMatchInfo; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.services.config.ConfigurationService; /** * Transformer of CMISQL queries into real SQL queries for the actual database. */ public class CMISQLQueryMaker implements QueryMaker { private static final Log log = LogFactory.getLog(CMISQLQueryMaker.class); public static final String TYPE = "CMISQL"; public static final String CMIS_PREFIX = "cmis:"; public static final String NX_PREFIX = "nuxeo:"; public static final String DC_FRAGMENT_NAME = "dublincore"; public static final String DC_TITLE_KEY = "title"; public static final String DC_DESCRIPTION_KEY = "description"; public static final String DC_CREATOR_KEY = "creator"; public static final String DC_CREATED_KEY = "created"; public static final String DC_MODIFIED_KEY = "modified"; public static final String DC_LAST_CONTRIBUTOR_KEY = "lastContributor"; public static final String REL_FRAGMENT_NAME = "relation"; public static final String REL_SOURCE_KEY = "source"; public static final String REL_TARGET_KEY = "target"; // list of SQL column where NULL (missing value) should be treated as // Boolean.FALSE public static final Set<String> NULL_IS_FALSE_COLUMNS = new HashSet<>( Arrays.asList(Model.HIER_TABLE_NAME + " " + Model.MAIN_IS_VERSION_KEY, Model.VERSION_TABLE_NAME + " " + Model.VERSION_IS_LATEST_KEY, Model.VERSION_TABLE_NAME + " " + Model.VERSION_IS_LATEST_MAJOR_KEY, Model.HIER_TABLE_NAME + " " + Model.MAIN_CHECKED_IN_KEY)); /** * These mixins never match an instance mixin when used in a clause nuxeo:secondaryObjectTypeIds = 'foo' */ protected static final Set<String> MIXINS_NOT_PER_INSTANCE = new HashSet<>( Arrays.asList(FacetNames.FOLDERISH, FacetNames.HIDDEN_IN_NAVIGATION)); protected Database database; protected Dialect dialect; protected Model model; protected Table hierTable; public boolean skipDeleted = true; // ----- filled during walks of the clauses ----- protected QueryUtilStrict queryUtil; protected QueryObject query; protected FulltextMatchInfo fulltextMatchInfo; protected Set<String> lifecycleWhereClauseQualifiers = new HashSet<>(); protected Set<String> mixinTypeWhereClauseQualifiers = new HashSet<>(); /** Qualifier to type. */ protected Map<String, String> qualifierToType = new HashMap<>(); /** Qualifier to canonical qualifier (correlation name). */ protected Map<String, String> canonicalQualifier = new HashMap<>(); /** Map of qualifier -> fragment -> table */ protected Map<String, Map<String, Table>> allTables = new HashMap<>(); /** All qualifiers used (includes virtual columns) */ protected Set<String> allQualifiers = new HashSet<>(); /** The qualifiers which correspond to versionable types. */ protected Set<String> versionableQualifiers = new HashSet<>(); /** The columns we'll actually request from the database. */ protected List<SqlColumn> realColumns = new LinkedList<>(); /** Parameters for above (for SCORE expressions on some databases) */ protected List<String> realColumnsParams = new LinkedList<>(); /** The non-real-columns we'll return as well. */ protected Map<String, ColumnReference> virtualColumns = new HashMap<>(); /** Type info returned to caller. */ protected Map<String, PropertyDefinition<?>> typeInfo = null; /** Search only latest version = !searchAllVersions. */ protected boolean searchLatestVersion = false; /** used for diagnostic when using DISTINCT */ protected List<String> virtualColumnNames = new LinkedList<>(); /** * Column corresponding to a returned value computed from an actual SQL expression. */ public static class SqlColumn { /** Column name or expression passed to SQL statement. */ public final String sql; /** Column used to get the value from the result set. */ public final Column column; /** Key for the value returned to the caller. */ public final String key; public SqlColumn(String sql, Column column, String key) { this.sql = sql; this.column = column; this.key = key; } } @Override public String getName() { return TYPE; } @Override public boolean accepts(String queryType) { return queryType.equals(TYPE); } /** * {@inheritDoc} * <p> * The optional parameters must be passed: {@code params[0]} is the {@link NuxeoCmisService}, optional * {@code params[1]} is a type info map, optional {@code params[2]} is searchAllVersions (default * {@code Boolean.TRUE} for this method). */ @Override public Query buildQuery(SQLInfo sqlInfo, Model model, PathResolver pathResolver, String statement, QueryFilter queryFilter, Object... params) { database = sqlInfo.database; dialect = sqlInfo.dialect; this.model = model; NuxeoCmisService service = (NuxeoCmisService) params[0]; if (params.length > 1) { typeInfo = (Map<String, PropertyDefinition<?>>) params[1]; } if (params.length > 2) { Boolean searchAllVersions = (Boolean) params[2]; searchLatestVersion = Boolean.FALSE.equals(searchAllVersions); } TypeManagerImpl typeManager = service.getTypeManager(); boolean addSystemColumns = true; // TODO hierTable = database.getTable(Model.HIER_TABLE_NAME); statement = applySecurityPolicyQueryTransformers(service, queryFilter.getPrincipal(), statement); try { queryUtil = new QueryUtilStrict(statement, typeManager, new AnalyzingWalker(), false); queryUtil.processStatement(); query = queryUtil.getQueryObject(); } catch (RecognitionException e) { throw new QueryParseException(queryUtil.getErrorMessage(e), e); } resolveQualifiers(); // now resolve column selectors to actual database columns for (CmisSelector sel : query.getSelectReferences()) { recordSelectSelector(sel); } for (CmisSelector sel : query.getJoinReferences()) { recordSelector(sel, JOIN); } for (CmisSelector sel : query.getWhereReferences()) { recordSelector(sel, WHERE); } for (SortSpec spec : query.getOrderBys()) { recordSelector(spec.getSelector(), ORDER_BY); } findVersionableQualifiers(); boolean distinct = false; // TODO extension addSystemColumns(addSystemColumns, distinct); /* * Find info about fragments needed. */ List<String> whereClauses = new LinkedList<String>(); List<Serializable> whereParams = new LinkedList<Serializable>(); /* * Walk joins. */ List<JoinSpec> joins = query.getJoins(); StringBuilder from = new StringBuilder(); List<Serializable> fromParams = new LinkedList<Serializable>(); for (int njoin = -1; njoin < joins.size(); njoin++) { JoinSpec join; boolean outerJoin; String alias; if (njoin == -1) { join = null; outerJoin = false; alias = query.getMainTypeAlias(); } else { join = joins.get(njoin); outerJoin = join.kind.equals("LEFT") || join.kind.equals("RIGHT"); alias = join.alias; } String typeQueryName = qualifierToType.get(alias); String qual = canonicalQualifier.get(alias); Table qualHierTable = getTable(hierTable, qual); // determine relevant primary types List<String> types = new ArrayList<String>(); TypeDefinition td = query.getTypeDefinitionFromQueryName(typeQueryName); if (td.getParentTypeId() != null) { // don't add abstract root types types.add(td.getId()); } LinkedList<TypeDefinitionContainer> typesTodo = new LinkedList<TypeDefinitionContainer>(); typesTodo.addAll(typeManager.getTypeDescendants(td.getId(), -1, Boolean.TRUE)); // recurse to get all subtypes TypeDefinitionContainer tc; while ((tc = typesTodo.poll()) != null) { types.add(tc.getTypeDefinition().getId()); typesTodo.addAll(tc.getChildren()); } if (types.isEmpty()) { // shoudn't happen types = Collections.singletonList("__NOSUCHTYPE__"); } // build clause StringBuilder qms = new StringBuilder(); for (int i = 0; i < types.size(); i++) { if (i != 0) { qms.append(", "); } qms.append("?"); } String primaryTypeClause = String.format("%s IN (%s)", qualHierTable.getColumn(Model.MAIN_PRIMARY_TYPE_KEY).getFullQuotedName(), qms); // table this join is about Table table; if (join == null) { table = qualHierTable; } else { // find which table in onLeft/onRight refers to current // qualifier table = null; for (ColumnReference col : Arrays.asList(join.onLeft, join.onRight)) { if (alias.equals(col.getQualifier())) { // TODO match with canonical qualifier instead? table = ((Column) col.getInfo()).getTable(); break; } } if (table == null) { throw new QueryParseException("Bad query, qualifier not found: " + qual); } } String tableName; if (table.isAlias()) { tableName = table.getRealTable().getQuotedName() + " " + table.getQuotedName(); } else { tableName = table.getQuotedName(); } boolean isRelation = table.getKey().equals(REL_FRAGMENT_NAME); // join clause on requested columns boolean primaryTypeClauseDone = false; if (join == null) { from.append(tableName); } else { if (outerJoin) { from.append(" "); from.append(join.kind); } from.append(" JOIN "); from.append(tableName); from.append(" ON ("); from.append(((Column) join.onLeft.getInfo()).getFullQuotedName()); from.append(" = "); from.append(((Column) join.onRight.getInfo()).getFullQuotedName()); if (outerJoin && table.getKey().equals(Model.HIER_TABLE_NAME)) { // outer join, type check must be part of JOIN from.append(" AND "); from.append(primaryTypeClause); fromParams.addAll(types); primaryTypeClauseDone = true; } from.append(")"); } // join other fragments for qualifier String tableMainId = table.getColumn(Model.MAIN_KEY).getFullQuotedName(); for (Table t : allTables.get(qual).values()) { if (t.getKey().equals(table.getKey())) { // already done above continue; } String n; if (t.isAlias()) { n = t.getRealTable().getQuotedName() + " " + t.getQuotedName(); } else { n = t.getQuotedName(); } from.append(" LEFT JOIN "); from.append(n); from.append(" ON ("); from.append(t.getColumn(Model.MAIN_KEY).getFullQuotedName()); from.append(" = "); from.append(tableMainId); if (outerJoin && t.getKey().equals(Model.HIER_TABLE_NAME)) { // outer join, type check must be part of JOIN from.append(" AND "); from.append(primaryTypeClause); fromParams.addAll(types); primaryTypeClauseDone = true; } from.append(")"); } // primary type clause, if not included in a JOIN if (!primaryTypeClauseDone) { whereClauses.add(primaryTypeClause); whereParams.addAll(types); } // lifecycle not deleted filter if (skipDeleted) { ModelProperty propertyInfo = model.getPropertyInfo(Model.MISC_LIFECYCLE_STATE_PROP); Column lscol = getTable(database.getTable(propertyInfo.fragmentName), qual).getColumn( propertyInfo.fragmentKey); String lscolName = lscol.getFullQuotedName(); whereClauses.add(String.format("(%s <> ? OR %s IS NULL)", lscolName, lscolName)); whereParams.add(LifeCycleConstants.DELETED_STATE); } // searchAllVersions filter boolean versionable = versionableQualifiers.contains(qual); if (searchLatestVersion && versionable) { // add islatestversion = true Table ver = getTable(database.getTable(Model.VERSION_TABLE_NAME), qual); Column latestvercol = ver.getColumn(Model.VERSION_IS_LATEST_KEY); String latestvercolName = latestvercol.getFullQuotedName(); whereClauses.add(String.format("(%s = ?)", latestvercolName)); whereParams.add(Boolean.TRUE); } // security check boolean checkSecurity = !isRelation // && queryFilter != null && queryFilter.getPrincipals() != null; if (checkSecurity) { Serializable principals; Serializable permissions; if (dialect.supportsArrays()) { principals = queryFilter.getPrincipals(); permissions = queryFilter.getPermissions(); } else { principals = StringUtils.join(queryFilter.getPrincipals(), '|'); permissions = StringUtils.join(queryFilter.getPermissions(), '|'); } if (dialect.supportsReadAcl()) { /* optimized read acl */ String readAclTable; String readAclTableAlias; String aclrumTable; String aclrumTableAlias; if (joins.size() == 0) { readAclTable = Model.HIER_READ_ACL_TABLE_NAME; readAclTableAlias = readAclTable; aclrumTable = Model.ACLR_USER_MAP_TABLE_NAME; aclrumTableAlias = aclrumTable; } else { readAclTableAlias = "nxr" + (njoin + 1); readAclTable = Model.HIER_READ_ACL_TABLE_NAME + ' ' + readAclTableAlias; // TODO dialect aclrumTableAlias = "aclrum" + (njoin + 1); aclrumTable = Model.ACLR_USER_MAP_TABLE_NAME + ' ' + aclrumTableAlias; // TODO dialect } String readAclIdCol = readAclTableAlias + '.' + Model.HIER_READ_ACL_ID; String readAclAclIdCol = readAclTableAlias + '.' + Model.HIER_READ_ACL_ACL_ID; String aclrumAclIdCol = aclrumTableAlias + '.' + Model.ACLR_USER_MAP_ACL_ID; String aclrumUserIdCol = aclrumTableAlias + '.' + Model.ACLR_USER_MAP_USER_ID; // first join with hierarchy_read_acl if (outerJoin) { from.append(" "); from.append(join.kind); } from.append(String.format(" JOIN %s ON (%s = %s)", readAclTable, tableMainId, readAclIdCol)); // second join with aclr_user_map String securityCheck = dialect.getReadAclsCheckSql(aclrumUserIdCol); String joinOn = String.format("%s = %s", readAclAclIdCol, aclrumAclIdCol); if (outerJoin) { from.append(" "); from.append(join.kind); // outer join, security check must be part of JOIN joinOn = String.format("%s AND %s", joinOn, securityCheck); fromParams.add(principals); } else { // inner join, security check can go in WHERE clause whereClauses.add(securityCheck); whereParams.add(principals); } from.append(String.format(" JOIN %s ON (%s)", aclrumTable, joinOn)); } else { String securityCheck = dialect.getSecurityCheckSql(tableMainId); if (outerJoin) { securityCheck = String.format("(%s OR %s IS NULL)", securityCheck, tableMainId); } whereClauses.add(securityCheck); whereParams.add(principals); whereParams.add(permissions); } } } /* * WHERE clause. */ Tree whereNode = queryUtil.getWalker().getWherePredicateTree(); if (whereNode != null) { GeneratingWalker generator = new GeneratingWalker(); generator.walkPredicate(whereNode); whereClauses.add(generator.whereBuf.toString()); whereParams.addAll(generator.whereBufParams); // add JOINs for the external fulltext matches Collections.sort(generator.ftJoins); // implicit JOINs last // (PostgreSQL) for (org.nuxeo.ecm.core.storage.sql.jdbc.db.Join join : generator.ftJoins) { from.append(join.toSql(dialect)); if (join.tableParam != null) { fromParams.add(join.tableParam); } } } /* * SELECT clause. */ List<String> selectWhat = new ArrayList<String>(); List<Serializable> selectParams = new ArrayList<Serializable>(1); for (SqlColumn rc : realColumns) { selectWhat.add(rc.sql); } selectParams.addAll(realColumnsParams); CMISQLMapMaker mapMaker = new CMISQLMapMaker(realColumns, virtualColumns, service); String what = StringUtils.join(selectWhat, ", "); if (distinct) { what = "DISTINCT " + what; } /* * ORDER BY clause. */ List<String> orderbys = new LinkedList<String>(); for (SortSpec spec : query.getOrderBys()) { String orderby; CmisSelector sel = spec.getSelector(); if (sel instanceof ColumnReference) { Column column = (Column) sel.getInfo(); orderby = column.getFullQuotedName(); } else { orderby = fulltextMatchInfo.scoreAlias; } if (!spec.ascending) { orderby += " DESC"; } orderbys.add(orderby); } /* * Create the whole select. */ Select select = new Select(null); select.setWhat(what); select.setFrom(from.toString()); // TODO(fromParams); // TODO add before whereParams select.setWhere(StringUtils.join(whereClauses, " AND ")); select.setOrderBy(StringUtils.join(orderbys, ", ")); Query q = new Query(); q.selectInfo = new SQLInfoSelect(select.getStatement(), mapMaker); q.selectParams = selectParams; q.selectParams.addAll(fromParams); q.selectParams.addAll(whereParams); return q; } /** * Applies security policies query transformers to the statement, if possible. Otherwise raises an exception. * * @since 5.7.2 * @throws CmisRuntimeException If a security policy prevents doing CMIS queries. */ protected String applySecurityPolicyQueryTransformers(NuxeoCmisService service, Principal principal, String statement) { SecurityPolicyService securityPolicyService = Framework.getLocalService(SecurityPolicyService.class); if (securityPolicyService == null) { return statement; } String repositoryId = service.getNuxeoRepository().getId(); for (SecurityPolicy policy : securityPolicyService.getPolicies()) { if (!policy.isRestrictingPermission(SecurityConstants.BROWSE)) { continue; } // check CMISQL transformer (new @since 5.7.2) if (!policy.isExpressibleInQuery(repositoryId, TYPE)) { throw new CmisRuntimeException( "Security policy " + policy.getClass().getName() + " prevents CMISQL execution"); } QueryTransformer transformer = policy.getQueryTransformer(repositoryId, TYPE); statement = transformer.transform(principal, statement); } return statement; } protected void findVersionableQualifiers() { List<JoinSpec> joins = query.getJoins(); for (int njoin = -1; njoin < joins.size(); njoin++) { boolean firstTable = njoin == -1; String alias; if (firstTable) { alias = query.getMainTypeAlias(); } else { alias = joins.get(njoin).alias; } String typeQueryName = qualifierToType.get(alias); TypeDefinition td = query.getTypeDefinitionFromQueryName(typeQueryName); boolean versionable = td.getBaseTypeId() == BaseTypeId.CMIS_DOCUMENT; if (versionable) { String qual = canonicalQualifier.get(alias); versionableQualifiers.add(qual); } } } protected boolean isFacetsColumn(String name) { return PropertyIds.SECONDARY_OBJECT_TYPE_IDS.equals(name) || NuxeoTypeHelper.NX_FACETS.equals(name); } // add main id to all qualifiers if // - we have no DISTINCT (in which case more columns don't matter), or // - we have virtual columns, or // - system columns are requested // check no added columns would bias the DISTINCT // after this method, allTables also contain hier table for virtual columns protected void addSystemColumns(boolean addSystemColumns, boolean distinct) { List<CmisSelector> addedSystemColumns = new ArrayList<CmisSelector>(2); for (String qual : allQualifiers) { TypeDefinition type = getTypeForQualifier(qual); // additional references to cmis:objectId and cmis:objectTypeId for (String propertyId : Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID)) { ColumnReference col = new ColumnReference(qual, propertyId); col.setTypeDefinition(propertyId, type); String key = getColumnKey(col); boolean add = true; for (SqlColumn rc : realColumns) { if (rc.key.equals(key)) { add = false; break; } } if (add) { addedSystemColumns.add(col); } } if (skipDeleted || lifecycleWhereClauseQualifiers.contains(qual)) { // add lifecycle state column ModelProperty propertyInfo = model.getPropertyInfo(Model.MISC_LIFECYCLE_STATE_PROP); Table table = getTable(database.getTable(propertyInfo.fragmentName), qual); recordFragment(qual, table); } if (mixinTypeWhereClauseQualifiers.contains(qual)) { recordFragment(qual, getTable(hierTable, qual)); } } // additional system columns to select on if (!distinct) { for (CmisSelector col : addedSystemColumns) { recordSelectSelector(col); } } else { if (!addedSystemColumns.isEmpty()) { if (!virtualColumnNames.isEmpty()) { throw new QueryParseException( "Cannot use DISTINCT with virtual columns: " + StringUtils.join(virtualColumnNames, ", ")); } if (addSystemColumns) { throw new QueryParseException("Cannot use DISTINCT without explicit " + PropertyIds.OBJECT_ID); } // don't add system columns as it would prevent DISTINCT from // working } } // for all qualifiers for (String qual : allQualifiers) { // include hier in fragments recordFragment(qual, getTable(hierTable, qual)); // if only latest version include the version table boolean versionable = versionableQualifiers.contains(qual); if (searchLatestVersion && versionable) { Table ver = database.getTable(Model.VERSION_TABLE_NAME); recordFragment(qual, getTable(ver, qual)); } } } /** * Records a SELECT selector, and associates it to a database column. */ protected void recordSelectSelector(CmisSelector sel) { if (sel instanceof FunctionReference) { FunctionReference fr = (FunctionReference) sel; if (fr.getFunction() != CmisQlFunction.SCORE) { throw new CmisRuntimeException("Unknown function: " + fr.getFunction()); } String key = fr.getAliasName(); if (key == null) { key = "SEARCH_SCORE"; // default, from spec } String scoreExprSql = fulltextMatchInfo.scoreExpr + " AS " + fulltextMatchInfo.scoreAlias; SqlColumn c = new SqlColumn(scoreExprSql, fulltextMatchInfo.scoreCol, key); realColumns.add(c); if (fulltextMatchInfo.scoreExprParam != null) { realColumnsParams.add(fulltextMatchInfo.scoreExprParam); } if (typeInfo != null) { PropertyDecimalDefinitionImpl pd = new PropertyDecimalDefinitionImpl(); pd.setId(key); pd.setQueryName(key); pd.setCardinality(Cardinality.SINGLE); pd.setDisplayName("Score"); pd.setLocalName("score"); typeInfo.put(key, pd); } } else { // sel instanceof ColumnReference ColumnReference col = (ColumnReference) sel; String qual = canonicalQualifier.get(col.getQualifier()); if (col.getPropertyQueryName().equals("*")) { TypeDefinition type = getTypeForQualifier(qual); for (PropertyDefinition<?> pd : type.getPropertyDefinitions().values()) { String id = pd.getId(); if ((pd.getCardinality() == Cardinality.SINGLE // && Boolean.TRUE.equals(pd.isQueryable())) || id.equals(PropertyIds.BASE_TYPE_ID)) { ColumnReference c = new ColumnReference(qual, id); c.setTypeDefinition(id, type); recordSelectSelector(c); } } return; } String key = getColumnKey(col); PropertyDefinition<?> pd = col.getPropertyDefinition(); Column column = getColumn(col); if (column != null && pd.getCardinality() == Cardinality.SINGLE) { col.setInfo(column); recordColumnFragment(qual, column); String sql = column.getFullQuotedName(); SqlColumn c = new SqlColumn(sql, column, key); realColumns.add(c); } else { virtualColumns.put(key, col); virtualColumnNames.add(key); allQualifiers.add(qual); } if (typeInfo != null) { typeInfo.put(key, pd); } } } protected static final String JOIN = "JOIN"; protected static final String WHERE = "WHERE"; protected static final String ORDER_BY = "ORDER BY"; /** * Records a JOIN / WHERE / ORDER BY selector, and associates it to a database column. */ protected void recordSelector(CmisSelector sel, String clauseType) { if (sel instanceof FunctionReference) { FunctionReference fr = (FunctionReference) sel; if (clauseType != ORDER_BY) { // == ok throw new QueryParseException("Cannot use function in " + clauseType + " clause: " + fr.getFunction()); } // ORDER BY SCORE, nothing further to record if (fulltextMatchInfo == null) { throw new QueryParseException("Cannot use ORDER BY SCORE without CONTAINS"); } return; } ColumnReference col = (ColumnReference) sel; PropertyDefinition<?> pd = col.getPropertyDefinition(); boolean multi = pd.getCardinality() == Cardinality.MULTI; // fetch column and associate it to the selector Column column = getColumn(col); if (!isFacetsColumn(col.getPropertyId()) && column == null) { throw new QueryParseException( "Cannot use column in " + clauseType + " clause: " + col.getPropertyQueryName()); } col.setInfo(column); String qual = canonicalQualifier.get(col.getQualifier()); if (clauseType == WHERE && NuxeoTypeHelper.NX_LIFECYCLE_STATE.equals(col.getPropertyId())) { // explicit lifecycle query: do not include the 'deleted' lifecycle // filter skipDeleted = false; lifecycleWhereClauseQualifiers.add(qual); } if (clauseType == WHERE && isFacetsColumn(col.getPropertyId())) { mixinTypeWhereClauseQualifiers.add(qual); } // record as a needed fragment if (!multi) { recordColumnFragment(qual, column); } } /** * Records a database column's fragment (to know what to join). */ protected void recordColumnFragment(String qual, Column column) { recordFragment(qual, column.getTable()); } /** * Records a database table and qualifier (to know what to join). */ protected void recordFragment(String qual, Table table) { String fragment = table.getKey(); Map<String, Table> tablesByFragment = allTables.get(qual); if (tablesByFragment == null) { allTables.put(qual, tablesByFragment = new HashMap<>()); } tablesByFragment.put(fragment, table); allQualifiers.add(qual); } /** * Finds what qualifiers are allowed and to what correlation name they are mapped. */ protected void resolveQualifiers() { Map<String, String> types = query.getTypes(); Map<String, AtomicInteger> typeCount = new HashMap<>(); for (Entry<String, String> en : types.entrySet()) { String qual = en.getKey(); String typeQueryName = en.getValue(); qualifierToType.put(qual, typeQueryName); // if an alias, use as its own correlation name canonicalQualifier.put(qual, qual); // also use alias as correlation name for this type // (ambiguous types removed later) canonicalQualifier.put(typeQueryName, qual); // count type use if (!typeCount.containsKey(typeQueryName)) { typeCount.put(typeQueryName, new AtomicInteger(0)); } typeCount.get(typeQueryName).incrementAndGet(); } for (Entry<String, AtomicInteger> en : typeCount.entrySet()) { String typeQueryName = en.getKey(); if (en.getValue().get() == 1) { // for types used once, allow direct type reference qualifierToType.put(typeQueryName, typeQueryName); } else { // ambiguous type, not legal as qualifier canonicalQualifier.remove(typeQueryName); } } // if only one type, allow omitted qualifier (null) if (types.size() == 1) { String typeQueryName = types.values().iterator().next(); qualifierToType.put(null, typeQueryName); // correlation name is actually null for all qualifiers for (String qual : qualifierToType.keySet()) { canonicalQualifier.put(qual, null); } } } /** * Finds a database column from a CMIS reference. */ protected Column getColumn(ColumnReference col) { String qual = canonicalQualifier.get(col.getQualifier()); String id = col.getPropertyId(); Column column; if (id.startsWith(CMIS_PREFIX) || id.startsWith(NX_PREFIX)) { column = getSystemColumn(qual, id); } else { ModelProperty propertyInfo = model.getPropertyInfo(id); boolean multi = propertyInfo.propertyType.isArray(); Table table = database.getTable(propertyInfo.fragmentName); String key = multi ? Model.COLL_TABLE_VALUE_KEY : propertyInfo.fragmentKey; column = getTable(table, qual).getColumn(key); } return column; } protected Column getSystemColumn(String qual, String id) { Column column = getSystemColumn(id); if (column != null && qual != null) { // alias table according to qualifier Table table = column.getTable(); column = getTable(table, qual).getColumn(column.getKey()); // TODO ensure key == name, or add getName() } return column; } protected Column getSystemColumn(String id) { if (id.equals(PropertyIds.OBJECT_ID)) { return hierTable.getColumn(Model.MAIN_KEY); } if (id.equals(PropertyIds.PARENT_ID)) { return hierTable.getColumn(Model.HIER_PARENT_KEY); } if (id.equals(NuxeoTypeHelper.NX_PARENT_ID)) { return hierTable.getColumn(Model.HIER_PARENT_KEY); } if (id.equals(NuxeoTypeHelper.NX_PATH_SEGMENT)) { return hierTable.getColumn(Model.HIER_CHILD_NAME_KEY); } if (id.equals(NuxeoTypeHelper.NX_POS)) { return hierTable.getColumn(Model.HIER_CHILD_POS_KEY); } if (id.equals(PropertyIds.OBJECT_TYPE_ID)) { // joinedHierTable return hierTable.getColumn(Model.MAIN_PRIMARY_TYPE_KEY); } if (id.equals(PropertyIds.VERSION_LABEL)) { return database.getTable(Model.VERSION_TABLE_NAME).getColumn(Model.VERSION_LABEL_KEY); } if (id.equals(PropertyIds.IS_LATEST_MAJOR_VERSION)) { return database.getTable(Model.VERSION_TABLE_NAME).getColumn(Model.VERSION_IS_LATEST_MAJOR_KEY); } if (id.equals(PropertyIds.IS_LATEST_VERSION)) { return database.getTable(Model.VERSION_TABLE_NAME).getColumn(Model.VERSION_IS_LATEST_KEY); } if (id.equals(NuxeoTypeHelper.NX_ISVERSION)) { return database.getTable(Model.HIER_TABLE_NAME).getColumn(Model.MAIN_IS_VERSION_KEY); } if (id.equals(NuxeoTypeHelper.NX_ISCHECKEDIN)) { return database.getTable(Model.HIER_TABLE_NAME).getColumn(Model.MAIN_CHECKED_IN_KEY); } if (id.equals(NuxeoTypeHelper.NX_LIFECYCLE_STATE)) { ModelProperty propertyInfo = model.getPropertyInfo(Model.MISC_LIFECYCLE_STATE_PROP); return database.getTable(propertyInfo.fragmentName).getColumn(propertyInfo.fragmentKey); } if (id.equals(PropertyIds.NAME)) { return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_TITLE_KEY); } if (id.equals(PropertyIds.DESCRIPTION)) { return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_DESCRIPTION_KEY); } if (id.equals(PropertyIds.CREATED_BY)) { return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_CREATOR_KEY); } if (id.equals(PropertyIds.CREATION_DATE)) { return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_CREATED_KEY); } if (id.equals(PropertyIds.LAST_MODIFICATION_DATE)) { return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_MODIFIED_KEY); } if (id.equals(PropertyIds.LAST_MODIFIED_BY)) { return database.getTable(DC_FRAGMENT_NAME).getColumn(DC_LAST_CONTRIBUTOR_KEY); } if (id.equals(PropertyIds.SOURCE_ID)) { return database.getTable(REL_FRAGMENT_NAME).getColumn(REL_SOURCE_KEY); } if (id.equals(PropertyIds.TARGET_ID)) { return database.getTable(REL_FRAGMENT_NAME).getColumn(REL_TARGET_KEY); } return null; } /** Get key to use in data returned to high-level caller. */ protected static String getColumnKey(ColumnReference col) { String alias = col.getAliasName(); if (alias != null) { return alias; } return getPropertyKey(col.getQualifier(), col.getPropertyQueryName()); } protected static String getPropertyKey(String qual, String id) { if (qual == null) { return id; } return qual + '.' + id; } protected TypeDefinition getTypeForQualifier(String qual) { String typeQueryName = qualifierToType.get(qual); return query.getTypeDefinitionFromQueryName(typeQueryName); } protected Table getTable(Table table, String qual) { if (qual == null) { return table; } else { return new TableAlias(table, getTableAlias(table, qual)); } } protected String getTableAlias(Table table, String qual) { return "_" + qual + "_" + table.getPhysicalName(); } /** * Map maker that can deal with aliased column names and computed values. */ // static to avoid keeping the whole QueryMaker in the returned object public static class CMISQLMapMaker implements MapMaker { protected List<SqlColumn> realColumns; protected Map<String, ColumnReference> virtualColumns; protected NuxeoCmisService service; public CMISQLMapMaker(List<SqlColumn> realColumns, Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) { this.realColumns = realColumns; this.virtualColumns = virtualColumns; this.service = service; } @Override public Map<String, Serializable> makeMap(ResultSet rs) throws SQLException { Map<String, Serializable> map = new HashMap<>(); // get values from result set int i = 1; for (SqlColumn rc : realColumns) { Serializable value = rc.column.getFromResultSet(rs, i++); String key = rc.column.getKey(); // type conversion to CMIS values if (value instanceof Long) { value = BigInteger.valueOf(((Long) value).longValue()); } else if (value instanceof Integer) { value = BigInteger.valueOf(((Integer) value).intValue()); } else if (value instanceof Double) { value = BigDecimal.valueOf(((Double) value).doubleValue()); } else if (value == null) { // special handling of some columns where NULL means FALSE String column = rc.column.getTable().getRealTable().getKey() + " " + key; if (NULL_IS_FALSE_COLUMNS.contains(column)) { value = Boolean.FALSE; } } if (Model.MAIN_KEY.equals(key) || Model.HIER_PARENT_KEY.equals(key)) { value = String.valueOf(value); // idToString } map.put(rc.key, value); } // virtual values // map to store actual data for each qualifier TypeManagerImpl typeManager = service.getTypeManager(); Map<String, NuxeoObjectData> datas = null; for (Entry<String, ColumnReference> vc : virtualColumns.entrySet()) { String key = vc.getKey(); ColumnReference col = vc.getValue(); String qual = col.getQualifier(); if (col.getPropertyId().equals(PropertyIds.BASE_TYPE_ID)) { // special case, no need to get full Nuxeo Document String typeId = (String) map.get(getPropertyKey(qual, PropertyIds.OBJECT_TYPE_ID)); TypeDefinitionContainer type = typeManager.getTypeById(typeId); String baseTypeId = type.getTypeDefinition().getBaseTypeId().value(); map.put(key, baseTypeId); continue; } if (datas == null) { datas = new HashMap<>(2); } NuxeoObjectData data = datas.get(qual); if (data == null) { // find main id for this qualifier in the result set // (main id always included in joins) // TODO check what happens if cmis:objectId is aliased String id = (String) map.get(getPropertyKey(qual, PropertyIds.OBJECT_ID)); try { // reentrant call to the same session, but the MapMaker // is only called from the IterableQueryResult in // queryAndFetch which manipulates no session state // TODO constructing the DocumentModel (in // NuxeoObjectData) is expensive, try to get value // directly data = (NuxeoObjectData) service.getObject(service.getNuxeoRepository().getId(), id, null, null, null, null, null, null, null); } catch (CmisRuntimeException e) { log.error("Cannot get document: " + id, e); } datas.put(qual, data); } Serializable v; if (data == null) { // could not fetch v = null; } else { NuxeoPropertyDataBase<?> pd = (NuxeoPropertyDataBase<?>) data.getProperty(col.getPropertyId()); if (pd == null) { v = null; } else { if (pd.getPropertyDefinition().getCardinality() == Cardinality.SINGLE) { v = (Serializable) pd.getFirstValue(); } else { v = (Serializable) pd.getValues(); } } } map.put(key, v); } return map; } } /** * Walker of the WHERE clause to gather fulltext info. */ public class AnalyzingWalker extends AbstractPredicateWalker { public static final String NX_FULLTEXT_INDEX_PREFIX = "nx:"; public boolean hasContains; @Override public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) { if (hasContains && Framework.getService(ConfigurationService.class) .isBooleanPropertyFalse(NuxeoRepository.RELAX_CMIS_SPEC)) { throw new QueryParseException("At most one CONTAINS() is allowed"); } hasContains = true; String qual = qualNode == null ? null : qualNode.getText(); qual = canonicalQualifier.get(qual); Column column = getSystemColumn(qual, PropertyIds.OBJECT_ID); String statement = (String) super.walkString(queryNode); String indexName = Model.FULLTEXT_DEFAULT_INDEX; // micro parsing of the fulltext statement to perform fulltext // search on a non default index if (statement.startsWith(NX_FULLTEXT_INDEX_PREFIX)) { statement = statement.substring(NX_FULLTEXT_INDEX_PREFIX.length()); int firstColumnIdx = statement.indexOf(':'); if (firstColumnIdx > 0 && firstColumnIdx < statement.length() - 1) { String requestedIndexName = statement.substring(0, firstColumnIdx); statement = statement.substring(firstColumnIdx + 1); if (model.getFulltextConfiguration().indexNames.contains(requestedIndexName)) { indexName = requestedIndexName; } else { throw new QueryParseException("No such fulltext index: " + requestedIndexName); } } else { log.warn(String.format("fail to microparse custom fulltext index:" + " fallback to '%s'", indexName)); } } // CMIS syntax to our internal google-like internal syntax statement = cmisToFulltextQuery(statement); // internal syntax to backend syntax statement = dialect.getDialectFulltextQuery(statement); fulltextMatchInfo = dialect.getFulltextScoredMatchInfo(statement, indexName, 1, column, model, database); return null; } } protected static String cmisToFulltextQuery(String statement) { // internal syntax has implicit AND statement = statement.replace(" and ", " "); statement = statement.replace(" AND ", " "); return statement; } /** * Walker of the WHERE clause that generates final SQL. */ public class GeneratingWalker extends AbstractPredicateWalker { public StringBuilder whereBuf = new StringBuilder(); public LinkedList<Serializable> whereBufParams = new LinkedList<Serializable>(); /** joins added by fulltext match */ public final List<org.nuxeo.ecm.core.storage.sql.jdbc.db.Join> ftJoins = new LinkedList<org.nuxeo.ecm.core.storage.sql.jdbc.db.Join>(); @Override public Boolean walkNot(Tree opNode, Tree node) { whereBuf.append("NOT "); walkPredicate(node); return null; } @Override public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) { whereBuf.append("("); walkPredicate(leftNode); whereBuf.append(" AND "); walkPredicate(rightNode); whereBuf.append(")"); return null; } @Override public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) { whereBuf.append("("); walkPredicate(leftNode); whereBuf.append(" OR "); walkPredicate(rightNode); whereBuf.append(")"); return null; } @Override public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) { if (isFacetsColumn(leftNode.getText())) { walkFacets(opNode, leftNode, rightNode); return null; } if (leftNode.getType() == CmisQlStrictLexer.COL && rightNode.getType() == CmisQlStrictLexer.BOOL_LIT && !Boolean.parseBoolean(rightNode.getText())) { // special handling of the " = false" case for column where // NULL means false walkIsNullOrFalse(leftNode); return null; } // normal case walkExpr(leftNode); whereBuf.append(" = "); walkExpr(rightNode); return null; } @Override public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) { if (leftNode.getType() == CmisQlStrictLexer.COL && rightNode.getType() == CmisQlStrictLexer.BOOL_LIT && Boolean.parseBoolean(rightNode.getText())) { // special handling of the " <> true" case for column where // NULL means false walkIsNullOrFalse(leftNode); return null; } walkExpr(leftNode); whereBuf.append(" <> "); walkExpr(rightNode); return null; } protected void walkIsNullOrFalse(Tree leftNode) { Column c = resolveColumn(leftNode); String columnSpec = c.getTable().getRealTable().getKey() + " " + c.getKey(); if (NULL_IS_FALSE_COLUMNS.contains(columnSpec)) { // treat NULL and FALSE as equivalent whereBuf.append("("); walkExpr(leftNode); whereBuf.append(" IS NULL OR "); walkExpr(leftNode); whereBuf.append(" = ?)"); whereBufParams.add(Boolean.FALSE); } else { // explicit false equality test walkExpr(leftNode); whereBuf.append(" = ?"); whereBufParams.add(Boolean.FALSE); } } @Override public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) { walkExpr(leftNode); whereBuf.append(" > "); walkExpr(rightNode); return null; } @Override public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) { walkExpr(leftNode); whereBuf.append(" >= "); walkExpr(rightNode); return null; } @Override public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) { walkExpr(leftNode); whereBuf.append(" < "); walkExpr(rightNode); return null; } @Override public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) { walkExpr(leftNode); whereBuf.append(" <= "); walkExpr(rightNode); return null; } @Override public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) { walkExpr(colNode); whereBuf.append(" IN "); walkExpr(listNode); return null; } @Override public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) { walkExpr(colNode); whereBuf.append(" NOT IN "); walkExpr(listNode); return null; } @Override public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) { if (isFacetsColumn(resolveColumnReference(colNode).getName())) { walkFacets(opNode, colNode, listNode); return null; } walkAny(colNode, "IN", listNode); return null; } @Override public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) { if (isFacetsColumn(resolveColumnReference(colNode).getName())) { walkFacets(opNode, colNode, listNode); return null; } walkAny(colNode, "NOT IN", listNode); return null; } @Override public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) { if (isFacetsColumn(resolveColumnReference(colNode).getName())) { walkFacets(opNode, colNode, literalNode); return null; } // note that argument order is reversed walkAny(colNode, "=", literalNode); return null; } protected void walkAny(Tree colNode, String op, Tree exprNode) { int token = ((Tree) colNode).getTokenStartIndex(); ColumnReference col = (ColumnReference) query.getColumnReference(Integer.valueOf(token)); PropertyDefinition<?> pd = col.getPropertyDefinition(); if (pd.getCardinality() != Cardinality.MULTI) { throw new QueryParseException( "Cannot use " + op + " ANY with single-valued property: " + col.getPropertyQueryName()); } Column column = (Column) col.getInfo(); String qual = canonicalQualifier.get(col.getQualifier()); // we need the real table and column in the subquery Table realTable = column.getTable().getRealTable(); Column realColumn = realTable.getColumn(column.getKey()); Column hierMainColumn = getTable(hierTable, qual).getColumn(Model.MAIN_KEY); Column multiMainColumn = realTable.getColumn(Model.MAIN_KEY); whereBuf.append("EXISTS (SELECT 1 FROM "); whereBuf.append(realTable.getQuotedName()); whereBuf.append(" WHERE "); whereBuf.append(hierMainColumn.getFullQuotedName()); whereBuf.append(" = "); whereBuf.append(multiMainColumn.getFullQuotedName()); whereBuf.append(" AND "); whereBuf.append(realColumn.getFullQuotedName()); whereBuf.append(" "); whereBuf.append(op); whereBuf.append(" "); walkExpr(exprNode); whereBuf.append(")"); } @Override public Boolean walkIsNull(Tree opNode, Tree colNode) { return walkIsNullOrIsNotNull(colNode, true); } @Override public Boolean walkIsNotNull(Tree opNode, Tree colNode) { return walkIsNullOrIsNotNull(colNode, false); } protected Boolean walkIsNullOrIsNotNull(Tree colNode, boolean isNull) { int token = ((Tree) colNode).getTokenStartIndex(); ColumnReference col = (ColumnReference) query.getColumnReference(Integer.valueOf(token)); PropertyDefinition<?> pd = col.getPropertyDefinition(); boolean multi = pd.getCardinality() == Cardinality.MULTI; if (multi) { // we need the real table and column in the subquery Column column = (Column) col.getInfo(); String qual = canonicalQualifier.get(col.getQualifier()); Table realTable = column.getTable().getRealTable(); Column hierMainColumn = getTable(hierTable, qual).getColumn(Model.MAIN_KEY); Column multiMainColumn = realTable.getColumn(Model.MAIN_KEY); if (isNull) { whereBuf.append("NOT "); } whereBuf.append("EXISTS (SELECT 1 FROM "); whereBuf.append(realTable.getQuotedName()); whereBuf.append(" WHERE "); whereBuf.append(hierMainColumn.getFullQuotedName()); whereBuf.append(" = "); whereBuf.append(multiMainColumn.getFullQuotedName()); whereBuf.append(')'); } else { walkExpr(colNode); whereBuf.append(isNull ? " IS NULL" : " IS NOT NULL"); } return null; } @Override public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) { walkExpr(colNode); whereBuf.append(" LIKE "); walkExpr(stringNode); return null; } @Override public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) { walkExpr(colNode); whereBuf.append(" NOT LIKE "); walkExpr(stringNode); return null; } @Override public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) { if (fulltextMatchInfo.joins != null) { ftJoins.addAll(fulltextMatchInfo.joins); } whereBuf.append(fulltextMatchInfo.whereExpr); if (fulltextMatchInfo.whereExprParam != null) { whereBufParams.add(fulltextMatchInfo.whereExprParam); } return null; } @Override public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) { String qual = qualNode == null ? null : qualNode.getText(); qual = canonicalQualifier.get(qual); // this is from the hierarchy table which is always present Column column = getSystemColumn(qual, PropertyIds.PARENT_ID); whereBuf.append(column.getFullQuotedName()); whereBuf.append(" = ?"); String id = (String) super.walkString(paramNode); whereBufParams.add(model.idFromString(id)); return null; } @Override public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) { String qual = qualNode == null ? null : qualNode.getText(); qual = canonicalQualifier.get(qual); // this is from the hierarchy table which is always present Column column = getSystemColumn(qual, PropertyIds.OBJECT_ID); String id = (String) super.walkString(paramNode); String sql = dialect.getInTreeSql(column.getFullQuotedName(), id); if (sql == null) { whereBuf.append("0=1"); } else { whereBuf.append(sql); whereBufParams.add(model.idFromString(id)); } return null; } @Override public Object walkList(Tree node) { whereBuf.append("("); for (int i = 0; i < node.getChildCount(); i++) { if (i != 0) { whereBuf.append(", "); } Tree child = node.getChild(i); walkExpr(child); } whereBuf.append(")"); return null; } @Override public Object walkBoolean(Tree node) { Serializable value = (Serializable) super.walkBoolean(node); whereBuf.append("?"); whereBufParams.add(value); return null; } @Override public Object walkNumber(Tree node) { Serializable value = (Serializable) super.walkNumber(node); whereBuf.append("?"); whereBufParams.add(value); return null; } @Override public Object walkString(Tree node) { Serializable value = (Serializable) super.walkString(node); whereBuf.append("?"); whereBufParams.add(value); return null; } @Override public Object walkTimestamp(Tree node) { Serializable value = (Serializable) super.walkTimestamp(node); whereBuf.append("?"); whereBufParams.add(value); return null; } @Override public Object walkCol(Tree node) { whereBuf.append(resolveColumn(node).getFullQuotedName()); return null; } public ColumnReference resolveColumnReference(Tree node) { int token = node.getTokenStartIndex(); CmisSelector sel = query.getColumnReference(Integer.valueOf(token)); if (sel instanceof ColumnReference) { return (ColumnReference) sel; } else { throw new QueryParseException("Cannot use column in WHERE clause: " + sel.getName()); } } public Column resolveColumn(Tree node) { return (Column) resolveColumnReference(node).getInfo(); } protected void walkFacets(Tree opNode, Tree colNodel, Tree literalNode) { boolean include; Set<String> mixins; int opType = opNode.getType(); if (opType == CmisQlStrictLexer.EQ_ANY) { include = true; if (literalNode.getType() != CmisQlStrictLexer.STRING_LIT) { throw new QueryParseException(colNodel.getText() + " = requires literal string as right argument"); } String value = super.walkString(literalNode).toString(); mixins = Collections.singleton(value); } else if (opType == CmisQlStrictLexer.IN_ANY || opType == CmisQlStrictLexer.NOT_IN_ANY) { include = opType == CmisQlStrictLexer.IN_ANY; mixins = new TreeSet<>(); for (int i = 0; i < literalNode.getChildCount(); i++) { mixins.add(super.walkString(literalNode.getChild(i)).toString()); } } else { throw new QueryParseException(colNodel.getText() + " unsupported operator: " + opNode.getText()); } /* * Primary types - static mixins */ Set<String> types; if (include) { types = new HashSet<>(); for (String mixin : mixins) { types.addAll(model.getMixinDocumentTypes(mixin)); } } else { types = new HashSet<>(model.getDocumentTypes()); for (String mixin : mixins) { types.removeAll(model.getMixinDocumentTypes(mixin)); } } /* * Instance mixins */ Set<String> instanceMixins = new HashSet<>(); for (String mixin : mixins) { if (!MIXINS_NOT_PER_INSTANCE.contains(mixin)) { instanceMixins.add(mixin); } } /* * SQL generation */ ColumnReference facetsCol = resolveColumnReference(colNodel); String qual = canonicalQualifier.get(facetsCol.getQualifier()); Table table = getTable(hierTable, qual); if (!types.isEmpty()) { Column col = table.getColumn(Model.MAIN_PRIMARY_TYPE_KEY); whereBuf.append(col.getFullQuotedName()); whereBuf.append(" IN "); whereBuf.append('('); for (Iterator<String> it = types.iterator(); it.hasNext();) { whereBuf.append('?'); whereBufParams.add(it.next()); if (it.hasNext()) { whereBuf.append(", "); } } whereBuf.append(')'); if (!instanceMixins.isEmpty()) { whereBuf.append(include ? " OR " : " AND "); } } if (!instanceMixins.isEmpty()) { whereBuf.append('('); Column mixinsColumn = table.getColumn(Model.MAIN_MIXIN_TYPES_KEY); String[] returnParam = new String[1]; for (Iterator<String> it = instanceMixins.iterator(); it.hasNext();) { String mixin = it.next(); String sql = dialect.getMatchMixinType(mixinsColumn, mixin, include, returnParam); whereBuf.append(sql); if (returnParam[0] != null) { whereBufParams.add(returnParam[0]); } if (it.hasNext()) { whereBuf.append(include ? " OR " : " AND "); } } if (!include) { whereBuf.append(" OR "); whereBuf.append(mixinsColumn.getFullQuotedName()); whereBuf.append(" IS NULL"); } whereBuf.append(')'); } if (types.isEmpty() && instanceMixins.isEmpty()) { whereBuf.append(include ? "0=1" : "0=0"); } } } }