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}