Skip to content

Commit 4cc085b

Browse files
committed
Optimize user list
1 parent 6fe81f9 commit 4cc085b

File tree

5 files changed

+271
-48
lines changed

5 files changed

+271
-48
lines changed

Kitodo/src/main/java/org/kitodo/production/forms/BaseForm.java

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -394,41 +394,6 @@ public String getFormattedDate(Date date) {
394394
return "";
395395
}
396396

397-
/**
398-
* Create and return String containing the titles of all given roles joined by a ", ".
399-
*
400-
* @param roles list of roles
401-
* @return String containing role titles
402-
*/
403-
public String getRoleTitles(List<Role> roles) {
404-
return RoleService.getRoleTitles(roles);
405-
}
406-
407-
/**
408-
* Create and return String containing the names of all given clients joined by a ", ".
409-
*
410-
* @param clients list of roles
411-
* @return String containing client names
412-
*/
413-
public String getClientNames(List<Client> clients) {
414-
return ClientService.getClientNames(clients);
415-
}
416-
417-
/**
418-
* Create and return String containing the titles of all given projects joined by a ", ".
419-
*
420-
* @param projects list of roles
421-
* @return String containing project titles
422-
*/
423-
public String getProjectTitles(List<Project> projects) {
424-
try {
425-
return ServiceManager.getProjectService().getProjectTitles(projects);
426-
} catch (DAOException e) {
427-
Helper.setErrorMessage(e);
428-
return "";
429-
}
430-
}
431-
432397
/**
433398
* Reset first row to 0 if given String 'keepPagination' is empty.
434399
*

Kitodo/src/main/java/org/kitodo/production/forms/UserForm.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
import org.kitodo.production.filters.FilterMenu;
6060
import org.kitodo.production.forms.dataeditor.GalleryViewMode;
6161
import org.kitodo.production.helper.Helper;
62-
import org.kitodo.production.model.LazyBeanModel;
62+
import org.kitodo.production.model.LazyUserModel;
6363
import org.kitodo.production.security.DynamicAuthenticationProvider;
6464
import org.kitodo.production.security.SecuritySession;
6565
import org.kitodo.production.security.password.KitodoPassword;
@@ -117,7 +117,7 @@ public class UserForm extends BaseForm {
117117
@Inject
118118
public UserForm(LoginForm loginForm) {
119119
super();
120-
super.setLazyBeanModel(new LazyBeanModel(userService));
120+
super.setLazyBeanModel(new LazyUserModel(userService));
121121
this.loginForm = loginForm;
122122
}
123123

@@ -288,14 +288,15 @@ public void resetTasksToOpen(User user) {
288288
* user from a connected LDAP service.
289289
*/
290290
public void checkAndDelete() {
291-
if (getTasksInProgress(userObject).isEmpty()) {
291+
if (!hasTasksInProgress(userObject)) {
292292
deleteUser(userObject);
293293
} else {
294294
PrimeFaces.current().ajax().update("usersTabView:confirmResetTasksDialog");
295295
PrimeFaces.current().executeScript("PF('confirmResetTasksDialog').show();");
296296
}
297297
}
298298

299+
299300
/**
300301
* Unassign all tasks in work from user and set their status back to open and delete the user.
301302
*/
@@ -796,6 +797,30 @@ public FilterMenu getFilterMenu() {
796797
return filterMenu;
797798
}
798799

800+
private LazyUserModel getLazyUserModel() {
801+
return (LazyUserModel) lazyBeanModel;
802+
}
803+
804+
public String getRoleTitles(User user) {
805+
List<String> roles = getLazyUserModel().getRolesCache().get(user.getId());
806+
return (roles == null || roles.isEmpty()) ? "" : String.join(", ", roles);
807+
}
808+
809+
public String getProjectTitles(User user) {
810+
List<String> projects = getLazyUserModel().getProjectsCache().get(user.getId());
811+
return (projects == null || projects.isEmpty()) ? "" : String.join(", ", projects);
812+
}
813+
814+
public String getClientNames(User user) {
815+
List<String> clients = getLazyUserModel().getClientsCache().get(user.getId());
816+
return (clients == null || clients.isEmpty()) ? "" : String.join(", ", clients);
817+
}
818+
819+
public boolean hasTasksInProgress(User user) {
820+
return Boolean.TRUE.equals(getLazyUserModel().getTasksCache().get(user.getId()));
821+
822+
}
823+
799824
private void deselectRoleClientColumn() {
800825
selectedColumns = ServiceManager.getListColumnService().removeColumnByTitle(selectedColumns, "role.client");
801826
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* (c) Kitodo. Key to digital objects e. V. <[email protected]>
3+
*
4+
* This file is part of the Kitodo project.
5+
*
6+
* It is licensed under GNU General Public License version 3 or later.
7+
*
8+
* For the full copyright and license information, please read the
9+
* GPL3-License.txt file that was distributed with this source code.
10+
*/
11+
12+
package org.kitodo.production.model;
13+
14+
import static java.lang.Math.toIntExact;
15+
16+
import java.util.HashMap;
17+
import java.util.LinkedList;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Objects;
21+
import java.util.stream.Collectors;
22+
23+
import org.apache.commons.lang3.StringUtils;
24+
import org.hibernate.exception.SQLGrammarException;
25+
import org.kitodo.data.database.beans.User;
26+
import org.kitodo.data.database.exceptions.DAOException;
27+
import org.kitodo.exceptions.FilterException;
28+
import org.kitodo.production.services.data.FilterService;
29+
import org.kitodo.production.services.data.UserService;
30+
import org.kitodo.utils.Stopwatch;
31+
import org.primefaces.PrimeFaces;
32+
import org.primefaces.model.FilterMeta;
33+
import org.primefaces.model.SortMeta;
34+
import org.primefaces.model.SortOrder;
35+
36+
public class LazyUserModel extends LazyBeanModel {
37+
38+
private final UserService userService;
39+
40+
private Map<Integer, List<String>> rolesCache = Map.of();
41+
private Map<Integer, List<String>> clientsCache = Map.of();
42+
private Map<Integer, List<String>> projectsCache = Map.of();
43+
private Map<Integer, Boolean> tasksCache = Map.of();
44+
45+
public LazyUserModel(UserService service) {
46+
super(service);
47+
this.userService = service; // ✔ store real type
48+
}
49+
50+
@Override
51+
@SuppressWarnings("unchecked")
52+
public List<Object> load(int first, int pageSize,
53+
Map<String, SortMeta> sortBy,
54+
Map<String, FilterMeta> filters) {
55+
56+
String sortField = "";
57+
SortOrder sortOrder = SortOrder.ASCENDING;
58+
if (!sortBy.isEmpty()) {
59+
SortMeta sortMeta = sortBy.values().iterator().next();
60+
sortField = sortMeta.getField();
61+
sortOrder = sortMeta.getOrder();
62+
}
63+
Stopwatch stopwatch = new Stopwatch(this, "load", "first", Integer.toString(first), "pageSize", Integer
64+
.toString(pageSize), "sortField", sortField, "sortOrder", Objects.toString(sortOrder), "filters",
65+
Objects.toString(filters));
66+
try {
67+
HashMap<String, String> filterMap = new HashMap<>();
68+
if (!StringUtils.isBlank(this.filterString)) {
69+
filterMap.put(FilterService.FILTER_STRING, this.filterString);
70+
}
71+
setRowCount(toIntExact(searchService.countResults(filterMap)));
72+
entities = searchService.loadData(first, pageSize, sortField, sortOrder, filterMap);
73+
rolesCache = Map.of();
74+
clientsCache = Map.of();
75+
projectsCache = Map.of();
76+
tasksCache = Map.of();
77+
List<Integer> ids = (List<Integer>) entities.stream()
78+
.map(u -> ((User)u).getId())
79+
.collect(Collectors.toList());
80+
// Load roles for all users in one query
81+
rolesCache = userService.loadRolesForUsers(ids);
82+
clientsCache = userService.loadClientsForUsers(ids);
83+
projectsCache = userService.loadProjectsForUsers(ids);
84+
tasksCache = userService.loadTasksInProgressForUsers(ids);
85+
logger.info("{} entities loaded!", entities.size());
86+
return stopwatch.stop(entities);
87+
} catch (DAOException | SQLGrammarException e) {
88+
setRowCount(0);
89+
logger.error(e.getMessage(), e);
90+
} catch (FilterException e) {
91+
setRowCount(0);
92+
PrimeFaces.current().executeScript("PF('sticky-notifications').renderMessage("
93+
+ "{'summary':'Filter error','detail':'" + e.getMessage() + "','severity':'error'});");
94+
logger.error(e.getMessage(), e);
95+
}
96+
return stopwatch.stop(new LinkedList<>());
97+
}
98+
99+
public Map<Integer, List<String>> getRolesCache() {
100+
return rolesCache;
101+
}
102+
103+
public Map<Integer, List<String>> getClientsCache() {
104+
return clientsCache;
105+
}
106+
107+
public Map<Integer, List<String>> getProjectsCache() {
108+
return projectsCache;
109+
}
110+
111+
public Map<Integer, Boolean> getTasksCache() {
112+
return tasksCache;
113+
}
114+
}
115+

Kitodo/src/main/java/org/kitodo/production/services/data/UserService.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public UserDetails loadUserByUsername(String username) {
138138
@Override
139139
public List<User> loadData(int first, int pageSize, String sortField, SortOrder sortOrder, Map filters) {
140140
HashMap<String, Object> filterMap;
141+
141142
try {
142143
filterMap = ServiceManager.getFilterService().getSQLFilterMap(filters, User.class);
143144
} catch (NoSuchFieldException | NumberFormatException e) {
@@ -560,4 +561,121 @@ public static List<Client> getClientsOfUserSorted(User user) {
560561
.sorted(Comparator.comparing(Client::getName, String.CASE_INSENSITIVE_ORDER))
561562
.collect(Collectors.toList());
562563
}
564+
565+
/**
566+
* Loads all role titles for the given user IDs in a single query.
567+
*
568+
* @param userIds list of user IDs
569+
* @return a map of userId and list of role titles
570+
*/
571+
public Map<Integer, List<String>> loadRolesForUsers(List<Integer> userIds) {
572+
if (userIds.isEmpty()) {
573+
return Collections.emptyMap();
574+
}
575+
String hql = "SELECT u.id, r.title "
576+
+ "FROM User u "
577+
+ "JOIN u.roles r "
578+
+ "WHERE u.id IN (:ids)";
579+
Map<String, Object> params = new HashMap<>();
580+
params.put("ids", userIds);
581+
List<Object[]> rows = dao.getProjectionByQuery(hql, params);
582+
Map<Integer, List<String>> result = new HashMap<>();
583+
for (Object[] row : rows) {
584+
Integer userId = (Integer) row[0];
585+
String title = (String) row[1];
586+
result.computeIfAbsent(userId, k -> new ArrayList<>()).add(title);
587+
}
588+
return result;
589+
}
590+
591+
/**
592+
* Loads all client names for the given user IDs in a single query.
593+
*
594+
* @param userIds list of user IDs
595+
* @return a map of userId and list of client names
596+
*/
597+
public Map<Integer, List<String>> loadClientsForUsers(List<Integer> userIds) {
598+
if (userIds.isEmpty()) {
599+
return Collections.emptyMap();
600+
}
601+
String hql = "SELECT u.id, c.name "
602+
+ "FROM User u "
603+
+ "JOIN u.clients c "
604+
+ "WHERE u.id IN (:ids)";
605+
Map<String, Object> params = new HashMap<>();
606+
params.put("ids", userIds);
607+
List<Object[]> rows = dao.getProjectionByQuery(hql, params);
608+
Map<Integer, List<String>> result = new HashMap<>();
609+
for (Object[] row : rows) {
610+
Integer userId = (Integer) row[0];
611+
String clientName = (String) row[1];
612+
result.computeIfAbsent(userId, k -> new ArrayList<>()).add(clientName);
613+
}
614+
return result;
615+
}
616+
617+
/**
618+
* Loads all project titles for the given user IDs in a single query.
619+
*
620+
* @param userIds list of user IDs
621+
* @return a map of userId and list of project titles
622+
*/
623+
public Map<Integer, List<String>> loadProjectsForUsers(List<Integer> userIds) {
624+
if (userIds.isEmpty()) {
625+
return Collections.emptyMap();
626+
}
627+
String hql = "SELECT u.id, p.title "
628+
+ "FROM User u "
629+
+ "JOIN u.projects p "
630+
+ "WHERE u.id IN (:ids)";
631+
Map<String, Object> params = new HashMap<>();
632+
params.put("ids", userIds);
633+
List<Object[]> rows = dao.getProjectionByQuery(hql, params);
634+
Map<Integer, List<String>> result = new HashMap<>();
635+
for (Object[] row : rows) {
636+
Integer userId = (Integer) row[0];
637+
String projectTitle = (String) row[1];
638+
result.computeIfAbsent(userId, k -> new ArrayList<>()).add(projectTitle);
639+
}
640+
return result;
641+
}
642+
643+
/**
644+
* Loads a boolean flag for each user ID indicating whether the user
645+
* has at least one task currently in progress.
646+
*
647+
* @param userIds list of user IDs
648+
* @return map of userId → true/false
649+
*/
650+
public Map<Integer, Boolean> loadTasksInProgressForUsers(List<Integer> userIds) {
651+
if (userIds.isEmpty()) {
652+
return Collections.emptyMap();
653+
}
654+
655+
String hql = "SELECT DISTINCT u.id "
656+
+ "FROM Task t "
657+
+ "JOIN t.processingUser u "
658+
+ "WHERE u.id IN (:ids) "
659+
+ "AND t.processingStatus = :status "
660+
+ "AND t.process IS NOT NULL";
661+
662+
Map<String, Object> params = new HashMap<>();
663+
params.put("ids", userIds);
664+
params.put("status", TaskStatus.INWORK);
665+
666+
List<Object> rows = dao.getProjectionByQuery(hql, params)
667+
.stream()
668+
.map(r -> (Object) r[0])
669+
.collect(Collectors.toList());
670+
671+
Map<Integer, Boolean> result = new HashMap<>();
672+
673+
// mark users who have tasks
674+
for (Object idObj : rows) {
675+
Integer userId = (Integer) idObj;
676+
result.put(userId, true);
677+
}
678+
679+
return result;
680+
}
563681
}

Kitodo/src/main/webapp/WEB-INF/templates/includes/users/userList.xhtml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,18 @@
9191
</p:column>
9292
<p:column headerText="#{msgs.roles}"
9393
rendered="#{UserForm.showColumn('user.roles')}">
94-
<h:outputText value="#{UserForm.getRoleTitles(item.roles)}"
95-
title="#{UserForm.getRoleTitles(item.roles)}"/>
94+
<h:outputText value="#{UserForm.getRoleTitles(item)}"
95+
title="#{UserForm.getRoleTitles(item)}"/>
9696
</p:column>
9797
<p:column headerText="#{msgs.clients}"
9898
rendered="#{UserForm.showColumn('user.clients')}">
99-
<h:outputText value="#{UserForm.getClientNames(item.clients)}"
100-
title="#{UserForm.getClientNames(item.clients)}"/>
99+
<h:outputText value="#{UserForm.getClientNames(item)}"
100+
title="#{UserForm.getClientNames(item)}"/>
101101
</p:column>
102102
<p:column headerText="#{msgs.projects}"
103103
rendered="#{UserForm.showColumn('user.projects')}">
104-
<h:outputText value="#{UserForm.getProjectTitles(item.projects)}"
105-
title="#{UserForm.getProjectTitles(item.projects)}"/>
104+
<h:outputText value="#{UserForm.getProjectTitles(item)}"
105+
title="#{UserForm.getProjectTitles(item)}"/>
106106
</p:column>
107107
<p:column headerText="#{msgs.loggedIn}"
108108
rendered="#{UserForm.showColumn('user.loggedIn')}"
@@ -138,14 +138,14 @@
138138

139139
<h:panelGroup styleClass="action"
140140
rendered="#{SecurityAccessController.hasAuthorityToUnassignTasks()}"
141-
title="#{empty UserForm.getTasksInProgress(item) ? msgs.userWithoutTasks : msgs.unassignTasks}">
141+
title="#{UserForm.hasTasksInProgress(item) ? msgs.unassignTasks : msgs.userWithoutTasks}">
142142
<p:commandLink id="unassignTasks"
143143
action="#{UserForm.resetTasksToOpen(item)}"
144-
styleClass="#{empty UserForm.getTasksInProgress(item) ? 'ui-state-disabled' : ''}"
145-
disabled="#{empty UserForm.getTasksInProgress(item)}"
144+
styleClass="#{UserForm.hasTasksInProgress(item) ? '' : 'ui-state-disabled'}"
145+
disabled="#{not UserForm.hasTasksInProgress(item)}"
146146
update="@this">
147147
<h:outputText><i class="fa fa-user-times"/></h:outputText>
148-
<p:confirm message=" #{msgs.confirmUnassignTasks}" header="#{msgs.confirmRelease}"/>
148+
<p:confirm message="#{msgs.confirmUnassignTasks}" header="#{msgs.confirmRelease}"/>
149149
</p:commandLink>
150150
</h:panelGroup>
151151

0 commit comments

Comments
 (0)