001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.db.generic; 029 030import org.opencms.db.CmsCompositeQueryFragment; 031import org.opencms.db.CmsDbUtil; 032import org.opencms.db.CmsPagingQuery; 033import org.opencms.db.CmsSelectQuery; 034import org.opencms.db.CmsSelectQuery.TableAlias; 035import org.opencms.db.CmsSimpleQueryFragment; 036import org.opencms.db.CmsSqlBooleanClause; 037import org.opencms.db.CmsStatementBuilder; 038import org.opencms.db.I_CmsQueryFragment; 039import org.opencms.file.CmsGroup; 040import org.opencms.file.CmsUserSearchParameters; 041import org.opencms.file.CmsUserSearchParameters.SearchKey; 042import org.opencms.file.CmsUserSearchParameters.SortKey; 043import org.opencms.i18n.CmsEncoder; 044import org.opencms.security.CmsOrganizationalUnit; 045import org.opencms.security.I_CmsPrincipal; 046import org.opencms.util.CmsPair; 047import org.opencms.util.CmsStringUtil; 048import org.opencms.util.CmsUUID; 049 050import java.util.Collection; 051import java.util.List; 052 053import com.google.common.base.Joiner; 054 055/** 056 * Default implementation of the user query builder.<p> 057 * 058 * @since 8.0.0 059 */ 060public class CmsUserQueryBuilder { 061 062 /** 063 * Creates a query for searching users.<p> 064 * 065 * @param searchParams the user search criteria 066 * @param countOnly if true, the query will only count the total number of results instead of returning them 067 * 068 * @return a pair consisting of the query string and its parameters 069 */ 070 public CmsPair<String, List<Object>> createUserQuery(CmsUserSearchParameters searchParams, boolean countOnly) { 071 072 CmsSelectQuery select = new CmsSelectQuery(); 073 TableAlias users = select.addTable(tabUsers(), "usr"); 074 if (countOnly) { 075 select.addColumn("COUNT(" + users.column(colId()) + ")"); 076 } else { 077 String[] columns = new String[] { 078 colId(), 079 colName(), 080 colPassword(), 081 colFirstName(), 082 colLastName(), 083 colEmail(), 084 colLastLogin(), 085 colFlags(), 086 colOu(), 087 colDateCreated()}; 088 for (String columnName : columns) { 089 select.addColumn(users.column(columnName)); 090 } 091 } 092 CmsOrganizationalUnit orgUnit = searchParams.getOrganizationalUnit(); 093 boolean recursive = searchParams.recursiveOrgUnits(); 094 095 if (orgUnit != null) { 096 addOrgUnitCondition(select, users, orgUnit, recursive); 097 } 098 if (searchParams.isFilterCore()) { 099 select.addCondition(createCoreCondition(users)); 100 } 101 addAllowedOuCondition(select, users, searchParams.getAllowedOus()); 102 addFlagCondition(select, users, searchParams.getFlags(), searchParams.keepCoreUsers()); 103 if (orgUnit != null) { 104 addWebuserCondition(select, orgUnit, users); 105 } 106 addSearchFilterCondition(select, users, searchParams); 107 addGroupCondition(select, users, searchParams); 108 if (countOnly) { 109 CmsStatementBuilder builder = new CmsStatementBuilder(); 110 select.visit(builder); 111 return CmsPair.create(builder.getQuery(), builder.getParameters()); 112 } else { 113 addSorting(select, users, searchParams); 114 return makePaged(select, searchParams); 115 } 116 } 117 118 /** 119 * Adds OU conditions to an SQL query.<p> 120 * 121 * @param select the query 122 * @param users the user table alias 123 * @param allowedOus the allowed ous 124 */ 125 protected void addAllowedOuCondition( 126 CmsSelectQuery select, 127 TableAlias users, 128 List<CmsOrganizationalUnit> allowedOus) { 129 130 if ((allowedOus != null) && !allowedOus.isEmpty()) { 131 CmsCompositeQueryFragment ouCondition = new CmsCompositeQueryFragment(); 132 ouCondition.setPrefix("("); 133 ouCondition.setSuffix(")"); 134 ouCondition.setSeparator(" OR "); 135 for (CmsOrganizationalUnit ou : allowedOus) { 136 String ouName = CmsStringUtil.joinPaths("/", ou.getName()); 137 ouCondition.add(new CmsSimpleQueryFragment(users.column(colOu()) + " = ? ", ouName)); 138 } 139 select.addCondition(ouCondition); 140 } 141 } 142 143 /** 144 * Adds flag checking conditions to an SQL query.<p> 145 * 146 * @param select the query 147 * @param users the user table alias 148 * @param flags the flags 149 * @param allowCore set to true if core users should not be filtered out 150 */ 151 protected void addFlagCondition(CmsSelectQuery select, TableAlias users, int flags, boolean allowCore) { 152 153 if (flags != 0) { 154 I_CmsQueryFragment condition = createFlagCondition(users, flags); 155 if (allowCore) { 156 I_CmsQueryFragment coreCondition = createCoreCondition(users); 157 select.addCondition(CmsSqlBooleanClause.makeOr(condition, coreCondition)); 158 } else { 159 select.addCondition(condition); 160 } 161 } 162 } 163 164 /** 165 * Adds group conditions to an SQL query.<p> 166 * 167 * @param select the query 168 * @param users the user table alias 169 * @param searchParams the search parameters 170 */ 171 protected void addGroupCondition(CmsSelectQuery select, TableAlias users, CmsUserSearchParameters searchParams) { 172 173 CmsGroup group = searchParams.getGroup(); 174 if (group != null) { 175 CmsUUID groupId = group.getId(); 176 TableAlias groupUsers = select.addTable(tabGroupUsers(), "groupusrs"); 177 select.addCondition( 178 new CmsSimpleQueryFragment(groupUsers.column(colGroupUserGroupId()) + " = ? ", groupId.toString())); 179 select.addCondition( 180 new CmsSimpleQueryFragment(groupUsers.column(colGroupUserUserId()) + " = " + users.column(colId()))); 181 if (searchParams.isFilterByGroupOu()) { 182 select.addCondition(new CmsSimpleQueryFragment(users.column(colOu()) + " = ? ", group.getOuFqn())); 183 } 184 } 185 CmsGroup notGroup = searchParams.getNotGroup(); 186 if (notGroup != null) { 187 CmsSimpleQueryFragment notGroupCondition = new CmsSimpleQueryFragment( 188 "NOT EXISTS (SELECT " 189 + getGroupUserSubqueryColumns() 190 + " FROM " 191 + tabGroupUsers() 192 + " GU WHERE GU." 193 + colGroupUserUserId() 194 + " = " 195 + users.column(colId()) 196 + " AND GU." 197 + colGroupUserGroupId() 198 + " = ?)", 199 notGroup.getId().toString()); 200 select.addCondition(notGroupCondition); 201 } 202 203 Collection<CmsGroup> anyGroups = searchParams.getAnyGroups(); 204 if ((anyGroups != null) && !anyGroups.isEmpty()) { 205 CmsCompositeQueryFragment groupClause = new CmsCompositeQueryFragment(); 206 groupClause.setSeparator(" OR "); 207 for (CmsGroup grp : anyGroups) { 208 groupClause.add( 209 new CmsSimpleQueryFragment("GU." + colGroupUserGroupId() + " = ?", grp.getId().toString())); 210 } 211 CmsCompositeQueryFragment existsClause = new CmsCompositeQueryFragment(); 212 existsClause.add( 213 new CmsSimpleQueryFragment( 214 "EXISTS (SELECT " 215 + getGroupUserSubqueryColumns() 216 + " FROM " 217 + tabGroupUsers() 218 + " GU WHERE GU." 219 + colGroupUserUserId() 220 + " = " 221 + users.column(colId()) 222 + " AND ")); 223 existsClause.add(groupClause); 224 existsClause.add(new CmsSimpleQueryFragment(" ) ")); 225 select.addCondition(existsClause); 226 } 227 Collection<CmsGroup> notAnyGroups = searchParams.getNotAnyGroups(); 228 if ((notAnyGroups != null) && (!notAnyGroups.isEmpty())) { 229 CmsCompositeQueryFragment groupClause = new CmsCompositeQueryFragment(); 230 groupClause.setPrefix("("); 231 groupClause.setSuffix(")"); 232 groupClause.setSeparator(" OR "); 233 for (CmsGroup grp : notAnyGroups) { 234 groupClause.add( 235 new CmsSimpleQueryFragment("GU." + colGroupUserGroupId() + " = ?", grp.getId().toString())); 236 } 237 CmsCompositeQueryFragment notExistsClause = new CmsCompositeQueryFragment(); 238 notExistsClause.add( 239 new CmsSimpleQueryFragment( 240 "NOT EXISTS (SELECT " 241 + getGroupUserSubqueryColumns() 242 + " FROM " 243 + tabGroupUsers() 244 + " GU WHERE GU." 245 + colGroupUserUserId() 246 + " = " 247 + users.column(colId()) 248 + " AND ")); 249 notExistsClause.add(groupClause); 250 notExistsClause.add(new CmsSimpleQueryFragment(" ) ")); 251 select.addCondition(notExistsClause); 252 } 253 } 254 255 /** 256 * Adds a check for an OU to an SQL query.<p> 257 * 258 * @param select the query 259 * @param users the user table alias 260 * @param orgUnit the organizational unit 261 * @param recursive if true, checks for sub-OUs too 262 */ 263 protected void addOrgUnitCondition( 264 CmsSelectQuery select, 265 TableAlias users, 266 CmsOrganizationalUnit orgUnit, 267 boolean recursive) { 268 269 String ouName = orgUnit.getName(); 270 String pattern = CmsOrganizationalUnit.SEPARATOR + ouName; 271 if (recursive) { 272 pattern += "%"; 273 } 274 select.addCondition(CmsDbUtil.columnLike(users.column(colOu()), pattern)); 275 } 276 277 /** 278 * Adds a search condition to a query.<p> 279 * 280 * @param select the query 281 * @param users the user table alias 282 * @param searchParams the search criteria 283 */ 284 protected void addSearchFilterCondition( 285 CmsSelectQuery select, 286 TableAlias users, 287 CmsUserSearchParameters searchParams) { 288 289 String searchFilter = searchParams.getSearchFilter(); 290 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(searchFilter)) { 291 boolean caseInsensitive = !searchParams.isCaseSensitive(); 292 if (caseInsensitive) { 293 searchFilter = searchFilter.toLowerCase(); 294 } 295 CmsCompositeQueryFragment searchCondition = new CmsCompositeQueryFragment(); 296 searchCondition.setSeparator(" OR "); 297 searchCondition.setPrefix("("); 298 searchCondition.setSuffix(")"); 299 //use coalesce in case any of the name columns are null 300 String patternExprTemplate = generateConcat( 301 "COALESCE(%1$s, '')", 302 "' '", 303 "COALESCE(%2$s, '')", 304 "' '", 305 "COALESCE(%3$s, '')"); 306 patternExprTemplate = wrapLower(patternExprTemplate, caseInsensitive); 307 308 String patternExpr = String.format( 309 patternExprTemplate, 310 users.column(colName()), 311 users.column(colFirstName()), 312 users.column(colLastName())); 313 String like = " LIKE ? ESCAPE '!' "; 314 String matchExpr = patternExpr + like; 315 searchFilter = "%" + CmsEncoder.escapeSqlLikePattern(searchFilter, '!') + '%'; 316 searchCondition.add(new CmsSimpleQueryFragment(matchExpr, searchFilter)); 317 for (SearchKey key : searchParams.getSearchKeys()) { 318 switch (key) { 319 case email: 320 searchCondition.add( 321 new CmsSimpleQueryFragment( 322 wrapLower(users.column(colEmail()), caseInsensitive) + like, 323 searchFilter)); 324 break; 325 case orgUnit: 326 searchCondition.add(new CmsSimpleQueryFragment( 327 wrapLower(users.column(colOu()), caseInsensitive) + like, 328 searchFilter)); 329 break; 330 default: 331 break; 332 } 333 } 334 select.addCondition(searchCondition); 335 } 336 } 337 338 /** 339 * Adds a sort order to an SQL query.<p> 340 * 341 * @param select the query 342 * @param users the user table alias 343 * @param searchParams the user search criteria 344 */ 345 protected void addSorting(CmsSelectQuery select, TableAlias users, CmsUserSearchParameters searchParams) { 346 347 boolean ascending = searchParams.isAscending(); 348 String ordering = getSortExpression(users, searchParams); 349 if (ascending) { 350 ordering += " ASC"; 351 } else { 352 ordering += " DESC"; 353 } 354 355 select.setOrdering(ordering); 356 } 357 358 /** 359 * Adds a check for the web user condition to an SQL query.<p> 360 * 361 * @param select the query 362 * @param orgUnit the organizational unit 363 * @param users the user table alias 364 */ 365 protected void addWebuserCondition(CmsSelectQuery select, CmsOrganizationalUnit orgUnit, TableAlias users) { 366 367 String webuserConditionTemplate; 368 if (orgUnit.hasFlagWebuser()) { 369 webuserConditionTemplate = "( %1$s >= 32768 AND %1$s < 65536 )"; 370 } else { 371 webuserConditionTemplate = "( %1$s < 32768 OR %1$s >= 65536 )"; 372 } 373 String webuserCondition = String.format(webuserConditionTemplate, users.column(colFlags())); 374 select.addCondition(webuserCondition); 375 } 376 377 /** 378 * Column name accessor.<p> 379 * 380 * @return the name of the column 381 */ 382 protected String colDateCreated() { 383 384 return "USER_DATECREATED"; 385 } 386 387 /** 388 * Column name accessor.<p> 389 * 390 * @return the name of the column 391 */ 392 protected String colEmail() { 393 394 return "USER_EMAIL"; 395 } 396 397 /** 398 * Column name accessor.<p> 399 * 400 * @return the name of the column 401 */ 402 protected String colFirstName() { 403 404 return "USER_FIRSTNAME"; 405 } 406 407 /** 408 * Column name accessor.<p> 409 * 410 * @return the name of the column 411 */ 412 protected String colFlags() { 413 414 return "USER_FLAGS"; 415 } 416 417 /** 418 * Column name accessor.<p> 419 * 420 * @return the name of the column 421 */ 422 protected String colGroupUserGroupId() { 423 424 return "GROUP_ID"; 425 } 426 427 /** 428 * Column name accessor.<p> 429 * 430 * @return the name of the column 431 */ 432 protected String colGroupUserUserId() { 433 434 return "USER_ID"; 435 } 436 437 /** 438 * Column name accessor.<p> 439 * 440 * @return the name of the column 441 */ 442 protected String colId() { 443 444 return "USER_ID"; 445 } 446 447 /** 448 * Column name accessor.<p> 449 * 450 * @return the name of the column 451 */ 452 protected String colLastLogin() { 453 454 return "USER_LASTLOGIN"; 455 } 456 457 /** 458 * Column name accessor.<p> 459 * 460 * @return the name of the column 461 */ 462 protected String colLastName() { 463 464 return "USER_LASTNAME"; 465 } 466 467 /** 468 * Column name accessor.<p> 469 * 470 * @return the name of the column 471 */ 472 protected String colName() { 473 474 return "USER_NAME"; 475 } 476 477 /** 478 * Column name accessor.<p> 479 * 480 * @return the name of the column 481 */ 482 protected String colOu() { 483 484 return "USER_OU"; 485 } 486 487 /** 488 * Column name accessor.<p> 489 * 490 * @return the name of the column 491 */ 492 protected String colPassword() { 493 494 return "USER_PASSWORD"; 495 } 496 497 /** 498 * Creates a core user check condition.<p> 499 * 500 * @param users the user table alias 501 * 502 * @return the resulting SQL expression 503 */ 504 protected I_CmsQueryFragment createCoreCondition(TableAlias users) { 505 506 return new CmsSimpleQueryFragment(users.column(colFlags()) + " <= " + I_CmsPrincipal.FLAG_CORE_LIMIT); 507 } 508 509 /** 510 * Creates an SQL flag check condition.<p> 511 * 512 * @param users the user table alias 513 * @param flags the flags to check 514 * 515 * @return the resulting SQL expression 516 */ 517 protected I_CmsQueryFragment createFlagCondition(TableAlias users, int flags) { 518 519 return new CmsSimpleQueryFragment( 520 users.column(colFlags()) + " & ? = ? ", 521 new Integer(flags), 522 new Integer(flags)); 523 } 524 525 /** 526 * Generates an SQL expression for concatenating several other SQL expressions.<p> 527 * 528 * @param expressions the expressions to concatenate 529 * 530 * @return the concat expression 531 */ 532 protected String generateConcat(String... expressions) { 533 534 return "CONCAT(" + Joiner.on(", ").join(expressions) + ")"; 535 } 536 537 /** 538 * Generates an SQL expression for trimming whitespace from the beginning and end of a string.<p> 539 * 540 * @param expression the expression to wrap 541 * 542 * @return the expression for trimming the given expression 543 */ 544 protected String generateTrim(String expression) { 545 546 return "TRIM(" + expression + ")"; 547 } 548 549 /** 550 * Returns the columns that should be returned by user subqueries.<p> 551 * 552 * @return the columns that should be returned by user subqueries 553 */ 554 protected String getGroupUserSubqueryColumns() { 555 556 return "*"; 557 } 558 559 /** 560 * Returns the expression used for sorting the results.<p> 561 * 562 * @param users the user table alias 563 * @param searchParams the search parameters 564 * 565 * @return the sorting expressiong 566 */ 567 protected String getSortExpression(TableAlias users, CmsUserSearchParameters searchParams) { 568 569 SortKey sortKey = searchParams.getSortKey(); 570 String ordering = users.column(colId()); 571 if (sortKey != null) { 572 switch (sortKey) { 573 case email: 574 ordering = users.column(colEmail()); 575 break; 576 case loginName: 577 ordering = users.column(colName()); 578 break; 579 case fullName: 580 ordering = getUserFullNameExpression(users); 581 break; 582 case lastLogin: 583 ordering = users.column(colLastLogin()); 584 break; 585 case orgUnit: 586 ordering = users.column(colOu()); 587 break; 588 case activated: 589 ordering = getUserActivatedExpression(users); 590 break; 591 case flagStatus: 592 ordering = getUserFlagExpression(users, searchParams.getSortFlags()); 593 break; 594 default: 595 break; 596 597 } 598 } 599 return ordering; 600 } 601 602 /** 603 * Returns an expression for checking whether a user is activated.<p> 604 * 605 * @param users the user table alias 606 * 607 * @return the expression for checking whether the user is activated 608 */ 609 protected String getUserActivatedExpression(TableAlias users) { 610 611 return "MOD(" + users.column(colFlags()) + ", 2)"; 612 } 613 614 /** 615 * Returns a bitwise AND expression with a fixed second operand.<p> 616 * 617 * @param users the user table alias 618 * @param flags the user flags 619 * @return the resulting SQL expression 620 */ 621 protected String getUserFlagExpression(TableAlias users, int flags) { 622 623 return users.column(colFlags()) + " & " + flags; 624 625 } 626 627 /** 628 * Returns the SQL expression for generating the user's full name in the format 629 * 'firstname lastname (loginname)'.<p> 630 * 631 * @param users the user table alias 632 * 633 * @return the expression for generating the user's full name 634 */ 635 protected String getUserFullNameExpression(TableAlias users) { 636 637 //use coalesce in case any of the name columns are null 638 String template = generateTrim( 639 generateConcat("COALESCE(%1$s, '')", "' '", "COALESCE(%2$s, '')", "' ('", "%3$s", "')'")); 640 return String.format( 641 template, 642 users.column(colFirstName()), 643 users.column(colLastName()), 644 users.column(colName())); 645 } 646 647 /** 648 * Creates a query which uses paging from another query.<p> 649 * 650 * @param select the base query 651 * @param params the query parameters 652 * 653 * @return the paged version of the query 654 */ 655 protected CmsPair<String, List<Object>> makePaged(CmsSelectQuery select, CmsUserSearchParameters params) { 656 657 CmsPagingQuery paging = new CmsPagingQuery(select); 658 paging.setUseWindowFunctions(useWindowFunctionsForPaging()); 659 int page = params.getPage(); 660 int pageSize = params.getPageSize(); 661 paging.setNameSubquery(shouldNameSubqueries()); 662 paging.setPaging(pageSize, page); 663 CmsStatementBuilder builder = new CmsStatementBuilder(); 664 paging.visit(builder); 665 return CmsPair.create(builder.getQuery(), builder.getParameters()); 666 } 667 668 /** 669 * Should return true if subqueries in a FROM clause should be named.<p> 670 * 671 * @return true if subqueries in a FROM clause should be named 672 */ 673 protected boolean shouldNameSubqueries() { 674 675 return false; 676 } 677 678 /** 679 * Table name accessor.<p> 680 * 681 * @return the name of a table 682 */ 683 protected String tabGroups() { 684 685 return "CMS_GROUPS"; 686 } 687 688 /** 689 * Table name accessor.<p> 690 * 691 * @return the name of a table 692 */ 693 protected String tabGroupUsers() { 694 695 return "CMS_GROUPUSERS"; 696 } 697 698 /** 699 * Table name accessor.<p> 700 * 701 * @return the name of a table 702 */ 703 protected String tabUsers() { 704 705 return "CMS_USERS"; 706 } 707 708 /** 709 * Returns true if window functions should be used for paging.<p> 710 * 711 * @return true if window functions should be used for paging 712 */ 713 protected boolean useWindowFunctionsForPaging() { 714 715 return false; 716 } 717 718 /** 719 * Wraps an SQL expression in a "LOWER" call conditionally.<p> 720 * 721 * @param expr the expression to wrap 722 * @param caseInsensitive if false, no wrapping should occur 723 * 724 * @return the resulting expression 725 */ 726 protected String wrapLower(String expr, boolean caseInsensitive) { 727 728 return caseInsensitive ? "LOWER(" + expr + ")" : expr; 729 } 730 731}