diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java b/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java index ee4d05c74..61e10fc0e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java @@ -154,6 +154,16 @@ public class Storage { }); } + public CompletableFuture> loadUsers(Set uniqueIds) { + return future(() -> { + Map users = this.implementation.loadUsers(uniqueIds); + for (User user : users.values()) { + this.plugin.getEventDispatcher().dispatchUserLoad(user); + } + return users; + }); + } + public CompletableFuture saveUser(User user) { return future(() -> this.implementation.saveUser(user)); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java index 56f85b383..c8e27a046 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java @@ -69,6 +69,8 @@ public interface StorageImplementation { User loadUser(UUID uniqueId, String username) throws Exception; + Map loadUsers(Set uniqueIds) throws Exception; + void saveUser(User user) throws Exception; Set getUniqueUsers() throws Exception; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java index 908dab15c..69a2a1ca3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java @@ -67,6 +67,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -200,6 +201,16 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio return user; } + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + // add multithreading here? + Map map = new HashMap<>(); + for (UUID uniqueId : uniqueIds) { + map.put(uniqueId, loadUser(uniqueId, null)); + } + return map; + } + @Override public void saveUser(User user) throws IOException { user.normalData().discardChanges(); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java index 574df62f2..ab384c405 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java @@ -72,6 +72,7 @@ import org.bson.Document; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -328,6 +329,16 @@ public class MongoStorage implements StorageImplementation { return user; } + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + // make this a bulk search? + Map map = new HashMap<>(); + for (UUID uniqueId : uniqueIds) { + map.put(uniqueId, loadUser(uniqueId, null)); + } + return map; + } + @Override public void saveUser(User user) { MongoCollection c = this.database.getCollection(this.prefix + "users"); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java index 81c16779d..5b2584c84 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java @@ -152,6 +152,11 @@ public class SplitStorage implements StorageImplementation { return implFor(SplitStorageType.USER).loadUser(uniqueId, username); } + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + return implFor(SplitStorageType.USER).loadUsers(uniqueIds); + } + @Override public void saveUser(User user) throws Exception { implFor(SplitStorageType.USER).saveUser(user); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java index ee7d653ab..30f839ac4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java @@ -84,6 +84,7 @@ public class SqlStorage implements StorageImplementation { private static final Type LIST_STRING_TYPE = new TypeToken>(){}.getType(); private static final String USER_PERMISSIONS_SELECT = "SELECT id, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE uuid=?"; + private static final String USER_PERMISSIONS_SELECT_MULTIPLE = "SELECT uuid, id, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE "; private static final String USER_PERMISSIONS_DELETE_SPECIFIC = "DELETE FROM '{prefix}user_permissions' WHERE id=?"; private static final String USER_PERMISSIONS_DELETE_SPECIFIC_PROPS = "DELETE FROM '{prefix}user_permissions' WHERE uuid=? AND permission=? AND value=? AND server=? AND world=? AND expiry=? AND contexts=?"; private static final String USER_PERMISSIONS_DELETE = "DELETE FROM '{prefix}user_permissions' WHERE uuid=?"; @@ -99,6 +100,7 @@ public class SqlStorage implements StorageImplementation { private static final String PLAYER_SELECT_ALL_UUIDS_BY_USERNAME = "SELECT uuid FROM '{prefix}players' WHERE username=? AND NOT uuid=?"; private static final String PLAYER_DELETE_ALL_UUIDS_BY_USERNAME = "DELETE FROM '{prefix}players' WHERE username=? AND NOT uuid=?"; private static final String PLAYER_SELECT_BY_UUID = "SELECT username, primary_group FROM '{prefix}players' WHERE uuid=? LIMIT 1"; + private static final String PLAYER_SELECT_BY_UUID_MULTIPLE = "SELECT uuid, username, primary_group FROM '{prefix}players' WHERE "; private static final String PLAYER_SELECT_PRIMARY_GROUP_BY_UUID = "SELECT primary_group FROM '{prefix}players' WHERE uuid=? LIMIT 1"; private static final String PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID = "UPDATE '{prefix}players' SET primary_group=? WHERE uuid=?"; @@ -319,16 +321,38 @@ public class SqlStorage implements StorageImplementation { @Override public User loadUser(UUID uniqueId, String username) throws SQLException { - User user = this.plugin.getUserManager().getOrMake(uniqueId, username); - List nodes; SqlPlayerData playerData; try (Connection c = this.connectionFactory.getConnection()) { - nodes = selectUserPermissions(c, user.getUniqueId()); - playerData = selectPlayerData(c, user.getUniqueId()); + nodes = selectUserPermissions(c, uniqueId); + playerData = selectPlayerData(c, uniqueId); } + return createUser(uniqueId, username, playerData, nodes, true); + } + + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + Map> nodesMap; + Map playerDataMap; + + try (Connection c = this.connectionFactory.getConnection()) { + nodesMap = selectUserPermissions(c, uniqueIds); + playerDataMap = selectPlayerData(c, uniqueIds); + } + + Map users = new HashMap<>(); + for (UUID uniqueId : uniqueIds) { + SqlPlayerData playerData = playerDataMap.get(uniqueId); + List nodes = nodesMap.get(uniqueId); + users.put(uniqueId, createUser(uniqueId, null, playerData, nodes, false)); + } + return users; + } + + private User createUser(UUID uniqueId, String username, SqlPlayerData playerData, List nodes, boolean saveAfterAudit) throws SQLException { + User user = this.plugin.getUserManager().getOrMake(uniqueId, username); if (playerData != null) { if (playerData.primaryGroup != null) { user.getPrimaryGroup().setStoredValue(playerData.primaryGroup); @@ -342,7 +366,7 @@ public class SqlStorage implements StorageImplementation { user.loadNodesFromStorage(nodes); this.plugin.getUserManager().giveDefaultIfNeeded(user); - if (user.auditTemporaryNodes()) { + if (user.auditTemporaryNodes() && saveAfterAudit) { saveUser(user); } @@ -866,6 +890,58 @@ public class SqlStorage implements StorageImplementation { } } + private Map> selectUserPermissions(Connection c, Set users) throws SQLException { + Map> map = new HashMap<>(); + for (UUID uuid : users) { + map.put(uuid, new ArrayList<>()); + } + + try (Statement s = c.createStatement()) { + String sql = createUserSelectWhereClause(USER_PERMISSIONS_SELECT_MULTIPLE, users); + try (ResultSet rs = s.executeQuery(sql)) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("uuid")); + Node node = readNode(rs); + if (node != null) { + map.get(uuid).add(readNode(rs)); + } + } + } + } + + return map; + } + + private Map selectPlayerData(Connection c, Set users) throws SQLException { + Map map = new HashMap<>(); + + try (Statement s = c.createStatement()) { + String sql = createUserSelectWhereClause(PLAYER_SELECT_BY_UUID_MULTIPLE, users); + try (ResultSet rs = s.executeQuery(sql)) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("uuid")); + SqlPlayerData data = new SqlPlayerData( + rs.getString("primary_group"), + rs.getString("username") + ); + map.put(uuid, data); + } + } + } + + return map; + } + + private String createUserSelectWhereClause(String baseQuery, Set users) { + String param = users.stream() + .map(uuid -> "'" + uuid + "'") + .collect(Collectors.joining(",", "uuid IN (", ")")); + + // we don't want to use preparedstatements because the parameter length is variable + // safe to do string concat/replacement because the UUID.toString value isn't injectable + return this.statementProcessor.apply(baseQuery) + param; + } + private void deleteUser(Connection c, UUID user) throws SQLException { try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_DELETE))) { ps.setString(1, user.toString()); diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java index f3982d2ed..b6d4556af 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java @@ -61,9 +61,11 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; @@ -257,17 +259,24 @@ public class WebEditorRequest { .distinct(); } - stream.filter(uuid -> !users.containsKey(uuid)) + Set uuids = stream + .filter(uuid -> !users.containsKey(uuid)) .sorted() .limit(MAX_USERS - users.size()) - .map(uuid -> plugin.getStorage().loadUser(uuid, null)) - .forEach(fut -> { - User user = fut.join(); - if (user != null) { - users.put(user.getUniqueId(), user); - plugin.getUserManager().getHouseKeeper().cleanup(user.getUniqueId()); - } - }); + .collect(Collectors.toSet()); + + if (uuids.isEmpty()) { + return; + } + + // load users in bulk from storage + Map loadedUsers = plugin.getStorage().loadUsers(uuids).join(); + users.putAll(loadedUsers); + + // schedule cleanup + for (UUID uniqueId : loadedUsers.keySet()) { + plugin.getUserManager().getHouseKeeper().cleanup(uniqueId); + } } }