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