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