From a779b31ca50807df4614a92297940bf32b5a13f9 Mon Sep 17 00:00:00 2001 From: Luck Date: Sat, 25 Jan 2020 16:02:37 +0000 Subject: [PATCH] Refactor SQL storage implementation --- .../lucko/luckperms/common/model/NodeMap.java | 9 +- .../common/model/PermissionHolder.java | 8 +- .../lucko/luckperms/common/model/Track.java | 2 + .../common/model/manager/AbstractManager.java | 8 + .../common/model/manager/Manager.java | 9 + .../luckperms/common/storage/Storage.java | 4 +- .../file/CombinedConfigurateStorage.java | 43 +- .../file/SeparatedConfigurateStorage.java | 41 +- .../implementation/mongodb/MongoStorage.java | 38 +- .../implementation/sql/SchemaReader.java | 69 ++ .../storage/implementation/sql/SqlNode.java | 10 +- .../implementation/sql/SqlStorage.java | 844 ++++++++---------- .../luckperms/common/util/Iterators.java | 20 +- .../{ThrowingRunnable.java => Throwing.java} | 11 +- 14 files changed, 533 insertions(+), 583 deletions(-) create mode 100644 common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java rename common/src/main/java/me/lucko/luckperms/common/util/{ThrowingRunnable.java => Throwing.java} (85%) diff --git a/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java index d81c6bdf5..0c4c1ff2f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java @@ -65,6 +65,7 @@ import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Stream; /** * A map of nodes held by a {@link PermissionHolder}. @@ -272,7 +273,7 @@ public final class NodeMap { this.inheritanceMap.remove(context); } - void setContent(Collection set) { + void setContent(Iterable set) { this.map.clear(); this.inheritanceMap.clear(); for (Node n : set) { @@ -280,6 +281,12 @@ public final class NodeMap { } } + void setContent(Stream stream) { + this.map.clear(); + this.inheritanceMap.clear(); + stream.forEach(this::add); + } + void setContent(Multimap multimap) { setContent(multimap.values()); } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java index 2ab98f756..ec731b86c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java @@ -71,6 +71,7 @@ import java.util.TreeSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; +import java.util.stream.Stream; /** * Represents an object that can hold permissions, (a user or group) @@ -233,11 +234,16 @@ public abstract class PermissionHolder { getPlugin().getEventDispatcher().dispatchDataRecalculate(this); } - public void setNodes(DataType type, Collection set) { + public void setNodes(DataType type, Iterable set) { getData(type).setContent(set); invalidateCache(); } + public void setNodes(DataType type, Stream stream) { + getData(type).setContent(stream); + invalidateCache(); + } + public void replaceNodes(DataType type, Multimap multimap) { getData(type).setContent(multimap); invalidateCache(); diff --git a/common/src/main/java/me/lucko/luckperms/common/model/Track.java b/common/src/main/java/me/lucko/luckperms/common/model/Track.java index 8485149a0..161e993eb 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/Track.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/Track.java @@ -46,6 +46,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; @@ -96,6 +97,7 @@ public final class Track { } public void setGroups(List groups) { + Objects.requireNonNull(groups, "groups"); this.groups.clear(); this.groups.addAll(groups); } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java index 19b8da411..1df7651d6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap; import me.lucko.luckperms.common.cache.LoadingMap; +import java.util.Collection; import java.util.Map; /** @@ -69,6 +70,13 @@ public abstract class AbstractManager implements Manager ids) { + this.objects.keySet().stream() + .filter(g -> !ids.contains(g)) + .forEach(this::unload); + } + protected I sanitizeIdentifier(I i) { return i; } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/Manager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/Manager.java index 71da306a1..8e93c3081 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/Manager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/Manager.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.common.model.manager; +import java.util.Collection; import java.util.Map; import java.util.function.Function; @@ -77,4 +78,12 @@ public interface Manager extends Function { */ void unload(I id); + /** + * Calls {@link #unload(Object)} for all objects currently + * loaded not in the given collection of ids. + * + * @param ids the ids to retain + */ + void retainAll(Collection ids); + } 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 f6fa9c54b..d14c3e2d9 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 @@ -36,7 +36,7 @@ import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.split.SplitStorage; -import me.lucko.luckperms.common.util.ThrowingRunnable; +import me.lucko.luckperms.common.util.Throwing; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; @@ -92,7 +92,7 @@ public class Storage { }, this.plugin.getBootstrap().getScheduler().async()); } - private CompletableFuture makeFuture(ThrowingRunnable runnable) { + private CompletableFuture makeFuture(Throwing.Runnable runnable) { return CompletableFuture.runAsync(() -> { try { runnable.run(); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java index e8e0a7af7..70374eee1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java @@ -27,13 +27,10 @@ package me.lucko.luckperms.common.storage.implementation.file; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.model.Group; -import me.lucko.luckperms.common.model.Track; -import me.lucko.luckperms.common.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.manager.track.TrackManager; import me.lucko.luckperms.common.node.model.HeldNodeImpl; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader; +import me.lucko.luckperms.common.util.Iterators; import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; @@ -290,32 +287,17 @@ public class CombinedConfigurateStorage extends AbstractConfigurateStorage { @Override public void loadAllGroups() throws IOException { List groups = new ArrayList<>(); - this.groupsLoader.apply(false, true, root -> { groups.addAll(root.getChildrenMap().keySet().stream() .map(Object::toString) .collect(Collectors.toList())); }); - boolean success = true; - for (String g : groups) { - try { - loadGroup(g); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(groups, this::loadGroup)) { throw new RuntimeException("Exception occurred whilst loading a group"); } - GroupManager gm = this.plugin.getGroupManager(); - gm.getAll().values().stream() - .map(Group::getName) - .filter(g -> !groups.contains(g)) - .forEach(gm::unload); + this.plugin.getGroupManager().retainAll(groups); } @Override @@ -345,32 +327,17 @@ public class CombinedConfigurateStorage extends AbstractConfigurateStorage { @Override public void loadAllTracks() throws IOException { List tracks = new ArrayList<>(); - this.tracksLoader.apply(false, true, root -> { tracks.addAll(root.getChildrenMap().keySet().stream() .map(Object::toString) .collect(Collectors.toList())); }); - boolean success = true; - for (String t : tracks) { - try { - loadTrack(t); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(tracks, this::loadTrack)) { throw new RuntimeException("Exception occurred whilst loading a track"); } - TrackManager tm = this.plugin.getTrackManager(); - tm.getAll().values().stream() - .map(Track::getName) - .filter(t -> !tracks.contains(t)) - .forEach(tm::unload); + this.plugin.getTrackManager().retainAll(tracks); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java index 2abe1158a..d51de582c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java @@ -27,14 +27,11 @@ package me.lucko.luckperms.common.storage.implementation.file; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.model.Group; -import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.manager.track.TrackManager; import me.lucko.luckperms.common.node.model.HeldNodeImpl; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader; +import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.MoreFiles; import me.lucko.luckperms.common.util.Uuids; @@ -292,25 +289,11 @@ public class SeparatedConfigurateStorage extends AbstractConfigurateStorage { .collect(Collectors.toList()); } - boolean success = true; - for (String g : groups) { - try { - loadGroup(g); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(groups, this::loadGroup)) { throw new RuntimeException("Exception occurred whilst loading a group"); } - GroupManager gm = this.plugin.getGroupManager(); - gm.getAll().values().stream() - .map(Group::getName) - .filter(g -> !groups.contains(g)) - .forEach(gm::unload); + this.plugin.getGroupManager().retainAll(groups); } @Override @@ -349,25 +332,11 @@ public class SeparatedConfigurateStorage extends AbstractConfigurateStorage { .collect(Collectors.toList()); } - boolean success = true; - for (String t : tracks) { - try { - loadTrack(t); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(tracks, this::loadTrack)) { throw new RuntimeException("Exception occurred whilst loading a track"); } - TrackManager tm = this.plugin.getTrackManager(); - tm.getAll().values().stream() - .map(Track::getName) - .filter(t -> !tracks.contains(t)) - .forEach(tm::unload); + this.plugin.getTrackManager().retainAll(tracks); } } 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 b936c6fe0..a6bb1bfb8 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 @@ -46,13 +46,13 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.manager.track.TrackManager; import me.lucko.luckperms.common.node.factory.NodeBuilders; import me.lucko.luckperms.common.node.model.HeldNodeImpl; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl; import me.lucko.luckperms.common.storage.misc.StorageCredentials; +import me.lucko.luckperms.common.util.Iterators; import net.luckperms.api.actionlog.Action; import net.luckperms.api.context.Context; @@ -442,25 +442,11 @@ public class MongoStorage implements StorageImplementation { } } - boolean success = true; - for (String g : groups) { - try { - loadGroup(g); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(groups, this::loadGroup)) { throw new RuntimeException("Exception occurred whilst loading a group"); } - GroupManager gm = this.plugin.getGroupManager(); - gm.getAll().values().stream() - .map(Group::getName) - .filter(g -> !groups.contains(g)) - .forEach(gm::unload); + this.plugin.getGroupManager().retainAll(groups); } @Override @@ -569,25 +555,11 @@ public class MongoStorage implements StorageImplementation { } } - boolean success = true; - for (String t : tracks) { - try { - loadTrack(t); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(tracks, this::loadTrack)) { throw new RuntimeException("Exception occurred whilst loading a track"); } - TrackManager tm = this.plugin.getTrackManager(); - tm.getAll().values().stream() - .map(Track::getName) - .filter(t -> !tracks.contains(t)) - .forEach(tm::unload); + this.plugin.getTrackManager().retainAll(tracks); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java new file mode 100644 index 000000000..bb7e48b00 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java @@ -0,0 +1,69 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.storage.implementation.sql; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; + +public final class SchemaReader { + private SchemaReader() {} + + public static List getStatements(InputStream is) throws IOException { + List queries = new LinkedList<>(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("--") || line.startsWith("#")) { + continue; + } + + sb.append(line); + + // check for end of declaration + if (line.endsWith(";")) { + sb.deleteCharAt(sb.length() - 1); + + String result = sb.toString().trim(); + if (!result.isEmpty()) { + queries.add(result); + } + + // reset + sb = new StringBuilder(); + } + } + } + + return queries; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java index 9d9f4fa7b..589611c42 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java @@ -47,6 +47,8 @@ import java.util.Set; */ public final class SqlNode { + public static final int NULL_ID = -1; + public static SqlNode fromNode(Node node) { ContextSet contexts = node.getContexts(); @@ -78,11 +80,7 @@ public final class SqlNode { long expiry = node.hasExpiry() ? node.getExpiry().getEpochSecond() : 0L; - return new SqlNode(node.getKey(), node.getValue(), server, world, expiry, contexts.immutableCopy(), -1); - } - - public static SqlNode fromSqlFields(String permission, boolean value, String server, String world, long expiry, String contexts) { - return new SqlNode(permission, value, server, world, expiry, ContextSetJsonSerializer.deserializeContextSet(GsonProvider.normal(), contexts).immutableCopy(), -1); + return new SqlNode(node.getKey(), node.getValue(), server, world, expiry, contexts.immutableCopy(), NULL_ID); } public static SqlNode fromSqlFields(long sqlId, String permission, boolean value, String server, String world, long expiry, String contexts) { @@ -142,7 +140,7 @@ public final class SqlNode { } public long getSqlId() { - if (this.sqlId == -1) { + if (this.sqlId == NULL_ID) { throw new IllegalStateException("sql id not set"); } return this.sqlId; 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 e6d098023..802bd81e3 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 @@ -25,7 +25,7 @@ package me.lucko.luckperms.common.storage.implementation.sql; -import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableMap; import com.google.gson.reflect.TypeToken; import me.lucko.luckperms.common.actionlog.Log; @@ -38,12 +38,12 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.manager.track.TrackManager; import me.lucko.luckperms.common.node.model.HeldNodeImpl; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl; +import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.gson.GsonProvider; import net.luckperms.api.actionlog.Action; @@ -52,11 +52,9 @@ import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; -import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.PreparedStatement; @@ -65,8 +63,8 @@ import java.sql.SQLException; import java.sql.Statement; import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -83,7 +81,7 @@ public class SqlStorage implements StorageImplementation { private static final String USER_PERMISSIONS_DELETE = "DELETE FROM '{prefix}user_permissions' WHERE uuid=?"; private static final String USER_PERMISSIONS_INSERT = "INSERT INTO '{prefix}user_permissions' (uuid, permission, value, server, world, expiry, contexts) VALUES(?, ?, ?, ?, ?, ?, ?)"; private static final String USER_PERMISSIONS_SELECT_DISTINCT = "SELECT DISTINCT uuid FROM '{prefix}user_permissions'"; - private static final String USER_PERMISSIONS_SELECT_PERMISSION = "SELECT uuid, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE "; + private static final String USER_PERMISSIONS_SELECT_PERMISSION = "SELECT uuid, id, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE "; private static final String PLAYER_SELECT_UUID_BY_USERNAME = "SELECT uuid FROM '{prefix}players' WHERE username=? LIMIT 1"; private static final String PLAYER_SELECT_USERNAME_BY_UUID = "SELECT username FROM '{prefix}players' WHERE uuid=? LIMIT 1"; @@ -99,13 +97,15 @@ public class SqlStorage implements StorageImplementation { private static final String GROUP_PERMISSIONS_DELETE_SPECIFIC = "DELETE FROM '{prefix}group_permissions' WHERE id=?"; private static final String GROUP_PERMISSIONS_DELETE = "DELETE FROM '{prefix}group_permissions' WHERE name=?"; private static final String GROUP_PERMISSIONS_INSERT = "INSERT INTO '{prefix}group_permissions' (name, permission, value, server, world, expiry, contexts) VALUES(?, ?, ?, ?, ?, ?, ?)"; - private static final String GROUP_PERMISSIONS_SELECT_PERMISSION = "SELECT name, permission, value, server, world, expiry, contexts FROM '{prefix}group_permissions' WHERE "; + private static final String GROUP_PERMISSIONS_SELECT_PERMISSION = "SELECT name, id, permission, value, server, world, expiry, contexts FROM '{prefix}group_permissions' WHERE "; private static final String GROUP_SELECT_ALL = "SELECT name FROM '{prefix}groups'"; - private static final String MYSQL_GROUP_INSERT = "INSERT INTO '{prefix}groups' (name) VALUES(?) ON DUPLICATE KEY UPDATE name=name"; - private static final String H2_GROUP_INSERT = "MERGE INTO '{prefix}groups' (name) VALUES(?)"; - private static final String SQLITE_GROUP_INSERT = "INSERT OR IGNORE INTO '{prefix}groups' (name) VALUES(?)"; - private static final String POSTGRESQL_GROUP_INSERT = "INSERT INTO '{prefix}groups' (name) VALUES(?) ON CONFLICT (name) DO NOTHING"; + private static final Map GROUP_INSERT = ImmutableMap.of( + "H2", "MERGE INTO '{prefix}groups' (name) VALUES(?)", + "SQLite", "INSERT OR IGNORE INTO '{prefix}groups' (name) VALUES(?)", + "PostgreSQL", "INSERT INTO '{prefix}groups' (name) VALUES(?) ON CONFLICT (name) DO NOTHING" + ); + private static final String GROUP_INSERT_DEFAULT = "INSERT INTO '{prefix}groups' (name) VALUES(?) ON DUPLICATE KEY UPDATE name=name"; private static final String GROUP_DELETE = "DELETE FROM '{prefix}groups' WHERE name=?"; private static final String TRACK_INSERT = "INSERT INTO '{prefix}tracks' (name, 'groups') VALUES(?, ?)"; @@ -146,86 +146,61 @@ public class SqlStorage implements StorageImplementation { return this.statementProcessor; } - private boolean tableExists(String table) throws SQLException { - try (Connection connection = this.connectionFactory.getConnection()) { - try (ResultSet rs = connection.getMetaData().getTables(null, null, "%", null)) { - while (rs.next()) { - if (rs.getString(3).equalsIgnoreCase(table)) { - return true; - } - } - return false; - } - } - } - @Override public void init() throws Exception { this.connectionFactory.init(); - // Init tables - if (!tableExists(this.statementProcessor.apply("{prefix}user_permissions"))) { - String schemaFileName = "me/lucko/luckperms/schema/" + this.connectionFactory.getImplementationName().toLowerCase() + ".sql"; - try (InputStream is = this.plugin.getBootstrap().getResourceStream(schemaFileName)) { - if (is == null) { - throw new Exception("Couldn't locate schema file for " + this.connectionFactory.getImplementationName()); + boolean tableExists; + try (Connection c = this.connectionFactory.getConnection()) { + tableExists = tableExists(c, this.statementProcessor.apply("{prefix}user_permissions")); + } + + if (!tableExists) { + applySchema(); + } + } + + private void applySchema() throws IOException, SQLException { + List statements; + + String schemaFileName = "me/lucko/luckperms/schema/" + this.connectionFactory.getImplementationName().toLowerCase() + ".sql"; + try (InputStream is = this.plugin.getBootstrap().getResourceStream(schemaFileName)) { + if (is == null) { + throw new IOException("Couldn't locate schema file for " + this.connectionFactory.getImplementationName()); + } + + statements = SchemaReader.getStatements(is).stream() + .map(this.statementProcessor) + .collect(Collectors.toList()); + } + + try (Connection connection = this.connectionFactory.getConnection()) { + boolean utf8mb4Unsupported = false; + + try (Statement s = connection.createStatement()) { + for (String query : statements) { + s.addBatch(query); } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - List queries = new LinkedList<>(); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("--") || line.startsWith("#")) { - continue; - } + try { + s.executeBatch(); + } catch (BatchUpdateException e) { + if (e.getMessage().contains("Unknown character set")) { + utf8mb4Unsupported = true; + } else { + throw e; + } + } + } - sb.append(line); - - // check for end of declaration - if (line.endsWith(";")) { - sb.deleteCharAt(sb.length() - 1); - - String result = this.statementProcessor.apply(sb.toString().trim()); - if (!result.isEmpty()) { - queries.add(result); - } - - // reset - sb = new StringBuilder(); - } + // try again + if (utf8mb4Unsupported) { + try (Statement s = connection.createStatement()) { + for (String query : statements) { + s.addBatch(query.replace("utf8mb4", "utf8")); } - try (Connection connection = this.connectionFactory.getConnection()) { - boolean utf8mb4Unsupported = false; - - try (Statement s = connection.createStatement()) { - for (String query : queries) { - s.addBatch(query); - } - - try { - s.executeBatch(); - } catch (BatchUpdateException e) { - if (e.getMessage().contains("Unknown character set")) { - utf8mb4Unsupported = true; - } else { - throw e; - } - } - } - - // try again - if (utf8mb4Unsupported) { - try (Statement s = connection.createStatement()) { - for (String query : queries) { - s.addBatch(query.replace("utf8mb4", "utf8")); - } - - s.executeBatch(); - } - } - } + s.executeBatch(); } } } @@ -249,13 +224,7 @@ public class SqlStorage implements StorageImplementation { public void logAction(Action entry) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(ACTION_INSERT))) { - ps.setLong(1, entry.getTimestamp().getEpochSecond()); - ps.setString(2, entry.getSource().getUniqueId().toString()); - ps.setString(3, entry.getSource().getName()); - ps.setString(4, Character.toString(LoggedAction.getTypeCharacter(entry.getTarget().getType()))); - ps.setString(5, entry.getTarget().getUniqueId().map(UUID::toString).orElse("null")); - ps.setString(6, entry.getTarget().getName()); - ps.setString(7, entry.getDescription()); + writeAction(entry, ps); ps.execute(); } } @@ -268,18 +237,7 @@ public class SqlStorage implements StorageImplementation { try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(ACTION_SELECT_ALL))) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - final String actedUuid = rs.getString("acted_uuid"); - LoggedAction e = LoggedAction.build() - .timestamp(Instant.ofEpochSecond(rs.getLong("time"))) - .source(UUID.fromString(rs.getString("actor_uuid"))) - .sourceName(rs.getString("actor_name")) - .targetType(LoggedAction.parseTypeCharacter(rs.getString("type").toCharArray()[0])) - .target(actedUuid.equals("null") ? null : UUID.fromString(actedUuid)) - .targetName(rs.getString("acted_name")) - .description(rs.getString("action")) - .build(); - - log.add(e); + log.add(readAction(rs)); } } } @@ -311,40 +269,17 @@ public class SqlStorage implements StorageImplementation { User user = this.plugin.getUserManager().getOrMake(uniqueId, username); user.getIoLock().lock(); try { - List data = new ArrayList<>(); + List nodes; String primaryGroup = null; - String userName = null; + String savedUsername = null; - // Collect user permissions try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_SELECT))) { - ps.setString(1, user.getUniqueId().toString()); + nodes = selectUserPermissions(new ArrayList<>(), c, user.getUniqueId()); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - String permission = rs.getString("permission"); - boolean value = rs.getBoolean("value"); - String server = rs.getString("server"); - String world = rs.getString("world"); - long expiry = rs.getLong("expiry"); - String contexts = rs.getString("contexts"); - data.add(SqlNode.fromSqlFields(permission, value, server, world, expiry, contexts)); - } - } - } - } - - // Collect user meta (username & primary group) - try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_BY_UUID))) { - ps.setString(1, user.getUniqueId().toString()); - - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - userName = rs.getString("username"); - primaryGroup = rs.getString("primary_group"); - } - } + SqlPlayerData playerData = selectPlayerData(c, user.getUniqueId()); + if (playerData != null) { + primaryGroup = playerData.primaryGroup; + savedUsername = playerData.username; } } @@ -355,12 +290,10 @@ public class SqlStorage implements StorageImplementation { user.getPrimaryGroup().setStoredValue(primaryGroup); // Update their username to what was in the storage if the one in the local instance is null - user.setUsername(userName, true); + user.setUsername(savedUsername, true); - // If the user has any data in storage - if (!data.isEmpty()) { - Set nodes = data.stream().map(SqlNode::toNode).collect(Collectors.toSet()); - user.setNodes(DataType.NORMAL, nodes); + if (!nodes.isEmpty()) { + user.setNodes(DataType.NORMAL, nodes.stream().map(SqlNode::toNode)); // Save back to the store if data they were given any defaults or had permissions expire if (this.plugin.getUserManager().giveDefaultIfNeeded(user, false) | user.auditTemporaryNodes()) { @@ -369,7 +302,6 @@ public class SqlStorage implements StorageImplementation { } } else { - // User has no data in storage. if (this.plugin.getUserManager().shouldSave(user)) { user.clearNodes(DataType.NORMAL, null, true); user.getPrimaryGroup().setStoredValue(null); @@ -386,107 +318,28 @@ public class SqlStorage implements StorageImplementation { public void saveUser(User user) throws SQLException { user.getIoLock().lock(); try { - // Empty data - just delete from the DB. if (!this.plugin.getUserManager().shouldSave(user)) { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_DELETE))) { - ps.setString(1, user.getUniqueId().toString()); - ps.execute(); - } - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { - ps.setString(1, GroupManager.DEFAULT_GROUP_NAME); - ps.setString(2, user.getUniqueId().toString()); - ps.execute(); - } + deleteUser(c, user.getUniqueId()); } return; } - // Get a snapshot of current data. - Set remote = new HashSet<>(); + Set remote; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_SELECT))) { - ps.setString(1, user.getUniqueId().toString()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - long id = rs.getLong("id"); - String permission = rs.getString("permission"); - boolean value = rs.getBoolean("value"); - String server = rs.getString("server"); - String world = rs.getString("world"); - long expiry = rs.getLong("expiry"); - String contexts = rs.getString("contexts"); - remote.add(SqlNode.fromSqlFields(id, permission, value, server, world, expiry, contexts)); - } - } - } + remote = selectUserPermissions(new HashSet<>(), c, user.getUniqueId()); } Set local = user.normalData().immutable().values().stream().map(SqlNode::fromNode).collect(Collectors.toSet()); - - Map.Entry, Set> diff = compareSets(local, remote); - - Set toAdd = diff.getKey(); - Set toRemove = diff.getValue(); - - if (!toRemove.isEmpty()) { - try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_DELETE_SPECIFIC))) { - for (SqlNode nd : toRemove) { - ps.setLong(1, nd.getSqlId()); - ps.addBatch(); - } - ps.executeBatch(); - } - } - } - - if (!toAdd.isEmpty()) { - try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_INSERT))) { - for (SqlNode nd : toAdd) { - ps.setString(1, user.getUniqueId().toString()); - ps.setString(2, nd.getPermission()); - ps.setBoolean(3, nd.getValue()); - ps.setString(4, nd.getServer()); - ps.setString(5, nd.getWorld()); - ps.setLong(6, nd.getExpiry()); - ps.setString(7, GsonProvider.normal().toJson(ContextSetJsonSerializer.serializeContextSet(nd.getContexts()))); - ps.addBatch(); - } - ps.executeBatch(); - } - } - } + Set missingFromRemote = getMissingFromRemote(local, remote); + Set missingFromLocal = getMissingFromLocal(local, remote); try (Connection c = this.connectionFactory.getConnection()) { - boolean hasPrimaryGroupSaved; - - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_PRIMARY_GROUP_BY_UUID))) { - ps.setString(1, user.getUniqueId().toString()); - try (ResultSet rs = ps.executeQuery()) { - hasPrimaryGroupSaved = rs.next(); - } - } - - if (hasPrimaryGroupSaved) { - // update - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { - ps.setString(1, user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME)); - ps.setString(2, user.getUniqueId().toString()); - ps.execute(); - } - } else { - // insert - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_INSERT))) { - ps.setString(1, user.getUniqueId().toString()); - ps.setString(2, user.getUsername().orElse("null").toLowerCase()); - ps.setString(3, user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME)); - ps.execute(); - } - } - + updateUserPermissions(c, user.getUniqueId(), missingFromRemote, missingFromLocal); + insertPlayerData(c, user.getUniqueId(), new SqlPlayerData( + user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME), + user.getUsername().orElse("null").toLowerCase() + )); } } finally { user.getIoLock().unlock(); @@ -520,15 +373,8 @@ public class SqlStorage implements StorageImplementation { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { UUID holder = UUID.fromString(rs.getString("uuid")); - String perm = rs.getString("permission"); - boolean value = rs.getBoolean("value"); - String server = rs.getString("server"); - String world = rs.getString("world"); - long expiry = rs.getLong("expiry"); - String contexts = rs.getString("contexts"); - - SqlNode data = SqlNode.fromSqlFields(perm, value, server, world, expiry, contexts); - held.add(HeldNodeImpl.of(holder, data.toNode())); + Node node = readNode(rs).toNode(); + held.add(HeldNodeImpl.of(holder, node)); } } } @@ -536,24 +382,10 @@ public class SqlStorage implements StorageImplementation { return held; } + @Override public Group createAndLoadGroup(String name) throws SQLException { - String query; - switch (this.connectionFactory.getImplementationName()) { - case "H2": - query = H2_GROUP_INSERT; - break; - case "SQLite": - query = SQLITE_GROUP_INSERT; - break; - case "PostgreSQL": - query = POSTGRESQL_GROUP_INSERT; - break; - default: - query = MYSQL_GROUP_INSERT; - break; - } - + String query = GROUP_INSERT.getOrDefault(this.connectionFactory.getImplementationName(), GROUP_INSERT_DEFAULT); try (Connection c = this.connectionFactory.getConnection()) { try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(query))) { ps.setString(1, name); @@ -566,17 +398,9 @@ public class SqlStorage implements StorageImplementation { @Override public Optional loadGroup(String name) throws SQLException { - // Check the group actually exists - List groups = new ArrayList<>(); + Set groups; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_SELECT_ALL))) { - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - groups.add(rs.getString("name").toLowerCase()); - } - } - } - + groups = selectGroups(c); } if (!groups.contains(name)) { @@ -586,29 +410,13 @@ public class SqlStorage implements StorageImplementation { Group group = this.plugin.getGroupManager().getOrMake(name); group.getIoLock().lock(); try { - List data = new ArrayList<>(); - + List nodes; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT))) { - ps.setString(1, group.getName()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - String permission = rs.getString("permission"); - boolean value = rs.getBoolean("value"); - String server = rs.getString("server"); - String world = rs.getString("world"); - long expiry = rs.getLong("expiry"); - String contexts = rs.getString("contexts"); - data.add(SqlNode.fromSqlFields(permission, value, server, world, expiry, contexts)); - } - } - } + nodes = selectGroupPermissions(new ArrayList<>(), c, group.getName()); } - if (!data.isEmpty()) { - Set nodes = data.stream().map(SqlNode::toNode).collect(Collectors.toSet()); - group.setNodes(DataType.NORMAL, nodes); + if (!nodes.isEmpty()) { + group.setNodes(DataType.NORMAL, nodes.stream().map(SqlNode::toNode)); } else { group.clearNodes(DataType.NORMAL, null, false); } @@ -620,108 +428,41 @@ public class SqlStorage implements StorageImplementation { @Override public void loadAllGroups() throws SQLException { - List groups = new ArrayList<>(); + Set groups; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_SELECT_ALL))) { - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - groups.add(rs.getString("name").toLowerCase()); - } - } - } + groups = selectGroups(c); } - boolean success = true; - for (String g : groups) { - try { - loadGroup(g); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(groups, this::loadGroup)) { throw new RuntimeException("Exception occurred whilst loading a group"); } - GroupManager gm = this.plugin.getGroupManager(); - gm.getAll().values().stream() - .map(Group::getName) - .filter(g -> !groups.contains(g)) - .forEach(gm::unload); + this.plugin.getGroupManager().retainAll(groups); } @Override public void saveGroup(Group group) throws SQLException { group.getIoLock().lock(); try { - // Empty data, just delete. if (group.normalData().immutable().isEmpty()) { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE))) { - ps.setString(1, group.getName()); - ps.execute(); - } + deleteGroupPermissions(c, group.getName()); } return; } - // Get a snapshot of current data - Set remote = new HashSet<>(); + Set remote; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT))) { - ps.setString(1, group.getName()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - long id = rs.getLong("id"); - String permission = rs.getString("permission"); - boolean value = rs.getBoolean("value"); - String server = rs.getString("server"); - String world = rs.getString("world"); - long expiry = rs.getLong("expiry"); - String contexts = rs.getString("contexts"); - remote.add(SqlNode.fromSqlFields(id, permission, value, server, world, expiry, contexts)); - } - } - } + remote = selectGroupPermissions(new HashSet<>(), c, group.getName()); } Set local = group.normalData().immutable().values().stream().map(SqlNode::fromNode).collect(Collectors.toSet()); + Set missingFromRemote = getMissingFromRemote(local, remote); + Set missingFromLocal = getMissingFromLocal(local, remote); - Map.Entry, Set> diff = compareSets(local, remote); - - Set toAdd = diff.getKey(); - Set toRemove = diff.getValue(); - - if (!toRemove.isEmpty()) { + if (!missingFromLocal.isEmpty() || !missingFromRemote.isEmpty()) { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE_SPECIFIC))) { - for (SqlNode nd : toRemove) { - ps.setLong(1, nd.getSqlId()); - ps.addBatch(); - } - ps.executeBatch(); - } - } - } - - if (!toAdd.isEmpty()) { - try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_INSERT))) { - for (SqlNode nd : toAdd) { - ps.setString(1, group.getName()); - ps.setString(2, nd.getPermission()); - ps.setBoolean(3, nd.getValue()); - ps.setString(4, nd.getServer()); - ps.setString(5, nd.getWorld()); - ps.setLong(6, nd.getExpiry()); - ps.setString(7, GsonProvider.normal().toJson(ContextSetJsonSerializer.serializeContextSet(nd.getContexts()))); - ps.addBatch(); - } - ps.executeBatch(); - } + updateGroupPermissions(c, group.getName(), missingFromRemote, missingFromLocal); } } } finally { @@ -734,10 +475,7 @@ public class SqlStorage implements StorageImplementation { group.getIoLock().lock(); try { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE))) { - ps.setString(1, group.getName()); - ps.execute(); - } + deleteGroupPermissions(c, group.getName()); try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_DELETE))) { ps.setString(1, group.getName()); @@ -762,15 +500,8 @@ public class SqlStorage implements StorageImplementation { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String holder = rs.getString("name"); - String perm = rs.getString("permission"); - boolean value = rs.getBoolean("value"); - String server = rs.getString("server"); - String world = rs.getString("world"); - long expiry = rs.getLong("expiry"); - String contexts = rs.getString("contexts"); - - SqlNode data = SqlNode.fromSqlFields(perm, value, server, world, expiry, contexts); - held.add(HeldNodeImpl.of(holder, data.toNode())); + Node node = readNode(rs).toNode(); + held.add(HeldNodeImpl.of(holder, node)); } } } @@ -783,31 +514,16 @@ public class SqlStorage implements StorageImplementation { Track track = this.plugin.getTrackManager().getOrMake(name); track.getIoLock().lock(); try { - boolean exists = false; - String groups = null; + List groups; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT))) { - ps.setString(1, track.getName()); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - exists = true; - groups = rs.getString("groups"); - } - } - } + groups = selectTrack(c, track.getName()); } - if (exists) { - // Track exists, let's load. - track.setGroups(GsonProvider.normal().fromJson(groups, LIST_STRING_TYPE)); + if (groups != null) { + track.setGroups(groups); } else { - String json = GsonProvider.normal().toJson(track.getGroups()); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_INSERT))) { - ps.setString(1, track.getName()); - ps.setString(2, json); - ps.execute(); - } + insertTrack(c, track.getName(), track.getGroups()); } } } finally { @@ -818,85 +534,50 @@ public class SqlStorage implements StorageImplementation { @Override public Optional loadTrack(String name) throws SQLException { - Track track = this.plugin.getTrackManager().getIfLoaded(name); - if (track != null) { - track.getIoLock().lock(); + Set tracks; + try (Connection c = this.connectionFactory.getConnection()) { + tracks = selectTracks(c); } + + if (!tracks.contains(name)) { + return Optional.empty(); + } + + Track track = this.plugin.getTrackManager().getOrMake(name); + track.getIoLock().lock(); try { - String groups; + List groups; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT))) { - ps.setString(1, name); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - groups = rs.getString("groups"); - } else { - return Optional.empty(); - } - } - } + groups = selectTrack(c, name); } - if (track == null) { - track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); - } - - track.setGroups(GsonProvider.normal().fromJson(groups, LIST_STRING_TYPE)); - return Optional.of(track); - + track.setGroups(groups); } finally { - if (track != null) { - track.getIoLock().unlock(); - } + track.getIoLock().unlock(); } + return Optional.of(track); } @Override public void loadAllTracks() throws SQLException { - List tracks = new ArrayList<>(); + Set tracks; try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT_ALL))) { - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - tracks.add(rs.getString("name").toLowerCase()); - } - } - } + tracks = selectTracks(c); } - boolean success = true; - for (String t : tracks) { - try { - loadTrack(t); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { + if (!Iterators.tryIterate(tracks, this::loadTrack)) { throw new RuntimeException("Exception occurred whilst loading a track"); } - TrackManager tm = this.plugin.getTrackManager(); - tm.getAll().values().stream() - .map(Track::getName) - .filter(t -> !tracks.contains(t)) - .forEach(tm::unload); + this.plugin.getTrackManager().retainAll(tracks); } @Override public void saveTrack(Track track) throws SQLException { track.getIoLock().lock(); try { - String s = GsonProvider.normal().toJson(track.getGroups()); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_UPDATE))) { - ps.setString(1, s); - ps.setString(2, track.getName()); - ps.execute(); - } + updateTrack(c, track.getName(), track.getGroups()); } } finally { track.getIoLock().unlock(); @@ -1008,23 +689,266 @@ public class SqlStorage implements StorageImplementation { return null; } - /** - * Compares two sets - * @param local the local set - * @param remote the remote set - * @return the entries to add to remote, and the entries to remove from remote - */ - private static Map.Entry, Set> compareSets(Set local, Set remote) { + private static void writeAction(Action action, PreparedStatement ps) throws SQLException { + ps.setLong(1, action.getTimestamp().getEpochSecond()); + ps.setString(2, action.getSource().getUniqueId().toString()); + ps.setString(3, action.getSource().getName()); + ps.setString(4, Character.toString(LoggedAction.getTypeCharacter(action.getTarget().getType()))); + ps.setString(5, action.getTarget().getUniqueId().map(UUID::toString).orElse("null")); + ps.setString(6, action.getTarget().getName()); + ps.setString(7, action.getDescription()); + } + + private static LoggedAction readAction(ResultSet rs) throws SQLException { + final String actedUuid = rs.getString("acted_uuid"); + return LoggedAction.build() + .timestamp(Instant.ofEpochSecond(rs.getLong("time"))) + .source(UUID.fromString(rs.getString("actor_uuid"))) + .sourceName(rs.getString("actor_name")) + .targetType(LoggedAction.parseTypeCharacter(rs.getString("type").toCharArray()[0])) + .target(actedUuid.equals("null") ? null : UUID.fromString(actedUuid)) + .targetName(rs.getString("acted_name")) + .description(rs.getString("action")) + .build(); + } + + private static SqlNode readNode(ResultSet rs) throws SQLException { + long id = rs.getLong("id"); + String permission = rs.getString("permission"); + boolean value = rs.getBoolean("value"); + String server = rs.getString("server"); + String world = rs.getString("world"); + long expiry = rs.getLong("expiry"); + String contexts = rs.getString("contexts"); + return SqlNode.fromSqlFields(id, permission, value, server, world, expiry, contexts); + } + + private static void writeNode(SqlNode nd, PreparedStatement ps) throws SQLException { + ps.setString(2, nd.getPermission()); + ps.setBoolean(3, nd.getValue()); + ps.setString(4, nd.getServer()); + ps.setString(5, nd.getWorld()); + ps.setLong(6, nd.getExpiry()); + ps.setString(7, GsonProvider.normal().toJson(ContextSetJsonSerializer.serializeContextSet(nd.getContexts()))); + } + + private static Set getMissingFromRemote(Set local, Set remote) { // entries in local but not remote need to be added + Set missingFromRemote = new HashSet<>(local); + missingFromRemote.removeAll(remote); + return missingFromRemote; + } + + private static Set getMissingFromLocal(Set local, Set remote) { // entries in remote but not local need to be removed + Set missingFromLocal = new HashSet<>(remote); + missingFromLocal.removeAll(local); + return missingFromLocal; + } - Set toAdd = new HashSet<>(local); - toAdd.removeAll(remote); + private > T selectUserPermissions(T nodes, Connection c, UUID user) throws SQLException { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_SELECT))) { + ps.setString(1, user.toString()); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + nodes.add(readNode(rs)); + } + } + } + return nodes; + } - Set toRemove = new HashSet<>(remote); - toRemove.removeAll(local); + private SqlPlayerData selectPlayerData(Connection c, UUID user) throws SQLException { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_BY_UUID))) { + ps.setString(1, user.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new SqlPlayerData(rs.getString("primary_group"), rs.getString("username")); + } else { + return null; + } + } + } + } - return Maps.immutableEntry(toAdd, toRemove); + 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()); + ps.execute(); + } + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { + ps.setString(1, GroupManager.DEFAULT_GROUP_NAME); + ps.setString(2, user.toString()); + ps.execute(); + } + } + + private void updateUserPermissions(Connection c, UUID user, Set add, Set delete) throws SQLException { + if (!delete.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_DELETE_SPECIFIC))) { + for (SqlNode node : delete) { + ps.setLong(1, node.getSqlId()); + ps.addBatch(); + } + ps.executeBatch(); + } + } + if (!add.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_INSERT))) { + for (SqlNode node : add) { + ps.setString(1, user.toString()); + writeNode(node, ps); + ps.addBatch(); + } + ps.executeBatch(); + } + } + } + + private void insertPlayerData(Connection c, UUID user, SqlPlayerData data) throws SQLException { + boolean hasPrimaryGroupSaved; + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_PRIMARY_GROUP_BY_UUID))) { + ps.setString(1, user.toString()); + try (ResultSet rs = ps.executeQuery()) { + hasPrimaryGroupSaved = rs.next(); + } + } + + if (hasPrimaryGroupSaved) { + // update + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { + ps.setString(1, data.primaryGroup); + ps.setString(2, user.toString()); + ps.execute(); + } + } else { + // insert + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_INSERT))) { + ps.setString(1, user.toString()); + ps.setString(2, data.username); + ps.setString(3, data.primaryGroup); + ps.execute(); + } + } + } + + private Set selectGroups(Connection c) throws SQLException { + Set groups = new HashSet<>(); + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_SELECT_ALL))) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + groups.add(rs.getString("name").toLowerCase()); + } + } + } + return groups; + } + + private > T selectGroupPermissions(T nodes, Connection c, String group) throws SQLException { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT))) { + ps.setString(1, group); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + nodes.add(readNode(rs)); + } + } + } + return nodes; + } + + private void deleteGroupPermissions(Connection c, String group) throws SQLException { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE))) { + ps.setString(1, group); + ps.execute(); + } + } + + private void updateGroupPermissions(Connection c, String group, Set add, Set delete) throws SQLException { + if (!delete.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE_SPECIFIC))) { + for (SqlNode node : delete) { + ps.setLong(1, node.getSqlId()); + ps.addBatch(); + } + ps.executeBatch(); + } + } + if (!add.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_INSERT))) { + for (SqlNode node : add) { + ps.setString(1, group); + writeNode(node, ps); + ps.addBatch(); + } + ps.executeBatch(); + } + } + } + + private List selectTrack(Connection c, String name) throws SQLException { + String groups; + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT))) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + groups = rs.getString("groups"); + } else { + groups = null; + } + } + } + return groups == null ? null : GsonProvider.normal().fromJson(groups, LIST_STRING_TYPE); + } + + private void insertTrack(Connection c, String name, List groups) throws SQLException { + String json = GsonProvider.normal().toJson(groups); + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_INSERT))) { + ps.setString(1, name); + ps.setString(2, json); + ps.execute(); + } + } + + private void updateTrack(Connection c, String name, List groups) throws SQLException { + String json = GsonProvider.normal().toJson(groups); + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_UPDATE))) { + ps.setString(1, json); + ps.setString(2, name); + ps.execute(); + } + } + + private Set selectTracks(Connection c) throws SQLException { + Set tracks = new HashSet<>(); + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT_ALL))) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + tracks.add(rs.getString("name").toLowerCase()); + } + } + } + return tracks; + } + + private static boolean tableExists(Connection connection, String table) throws SQLException { + try (ResultSet rs = connection.getMetaData().getTables(null, null, "%", null)) { + while (rs.next()) { + if (rs.getString(3).equalsIgnoreCase(table)) { + return true; + } + } + return false; + } + } + + private static final class SqlPlayerData { + private final String primaryGroup; + private final String username; + + SqlPlayerData(String primaryGroup, String username) { + this.primaryGroup = primaryGroup; + this.username = username; + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/Iterators.java b/common/src/main/java/me/lucko/luckperms/common/util/Iterators.java index 045186639..c7be962b8 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/Iterators.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/Iterators.java @@ -34,44 +34,56 @@ import java.util.function.Function; public final class Iterators { private Iterators() {} - public static void tryIterate(Iterable iterable, Consumer action) { + public static boolean tryIterate(Iterable iterable, Throwing.Consumer action) { + boolean success = true; for (E element : iterable) { try { action.accept(element); } catch (Exception e) { e.printStackTrace(); + success = false; } } + return success; } - public static void tryIterate(Iterable iterable, Function mapping, Consumer action) { + public static boolean tryIterate(Iterable iterable, Function mapping, Consumer action) { + boolean success = true; for (I element : iterable) { try { action.accept(mapping.apply(element)); } catch (Exception e) { e.printStackTrace(); + success = false; } } + return success; } - public static void tryIterate(E[] array, Consumer action) { + public static boolean tryIterate(E[] array, Consumer action) { + boolean success = true; for (E element : array) { try { action.accept(element); } catch (Exception e) { e.printStackTrace(); + success = false; } } + return success; } - public static void tryIterate(I[] array, Function mapping, Consumer action) { + public static boolean tryIterate(I[] array, Function mapping, Consumer action) { + boolean success = true; for (I element : array) { try { action.accept(mapping.apply(element)); } catch (Exception e) { e.printStackTrace(); + success = false; } } + return success; } public static List> divideIterable(Iterable source, int size) { diff --git a/common/src/main/java/me/lucko/luckperms/common/util/ThrowingRunnable.java b/common/src/main/java/me/lucko/luckperms/common/util/Throwing.java similarity index 85% rename from common/src/main/java/me/lucko/luckperms/common/util/ThrowingRunnable.java rename to common/src/main/java/me/lucko/luckperms/common/util/Throwing.java index d7def3576..a87483d63 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/ThrowingRunnable.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/Throwing.java @@ -25,8 +25,15 @@ package me.lucko.luckperms.common.util; -public interface ThrowingRunnable { +public interface Throwing { - void run() throws Exception; + @FunctionalInterface + interface Runnable { + void run() throws Exception; + } + @FunctionalInterface + interface Consumer { + void accept(T t) throws Exception; + } }