From 857cf6ad30912eb1f9b7cd4f49a07b6c92f43d97 Mon Sep 17 00:00:00 2001 From: cnr Date: Tue, 24 May 2016 19:18:40 -0500 Subject: [PATCH] Add PlayerKeyValueRepository and BukkitFuture PlayerKeyValueRepository is a key/value store whose keys are Strings and whose value type is parameterized by V. Each repository is backed by a MySQL table in the Accounts database. Access to PlayerKeyValueRepository's values is restricted via CompletableFuture to enforce async database access. BukkitFuture contains helpful utilities for producing, transforming, and terminating CompletableFutures with actions on the main thread. A typical PlayerKeyValueRepository action may look similar to the following, where we retrieve all key/value pairs for a player and perform an action with the result on the main thread: PlayerKeyValueRepository repo = [...]; // init repo UUID uuid = [...]; // a player's UUID repo.getAll(uuid).thenCompose(BukkitFuture.accept(values -> { // this will be run on the main thread! // `values` is of type `Map` })); --- .../core/common/util/BukkitFuture.java | 111 +++++++ .../database/PlayerKeyValueRepository.java | 291 ++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java create mode 100644 Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java diff --git a/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java b/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java new file mode 100644 index 000000000..e63644ac2 --- /dev/null +++ b/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java @@ -0,0 +1,111 @@ +package mineplex.core.common.util; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Utilities for interleaving Bukkit scheduler operations as + * intermediate and terminal operations in a {@link CompletionStage} + * pipeline. + *

+ * Any {@link Function}s returned by methods are suitable for use + * in {@link CompletionStage#thenCompose(Function)} + * + * @see CompletableFuture#thenCompose(Function) + */ +public class BukkitFuture +{ + private static final Plugin LOADING_PLUGIN = JavaPlugin.getProvidingPlugin(BukkitFuture.class); + + private static void runBlocking(Runnable action) + { + Bukkit.getScheduler().runTask(LOADING_PLUGIN, action); + } + + /** + * Finalize a {@link CompletionStage} by consuming its value + * on the main thread. + * + * @param action the {@link Consumer} to call on the main thread + * @return a {@link Function} to be passed as an argument to + * {@link CompletionStage#thenCompose(Function)} + * @see CompletableFuture#thenCompose(Function) + */ + public static Function> accept(Consumer action) + { + return val -> + { + CompletableFuture future = new CompletableFuture<>(); + runBlocking(() -> + { + action.accept(val); + future.complete(null); + }); + return future; + }; + } + + /** + * Finalize a {@link CompletionStage} by executing code on the + * main thread after its completion. + * + * @param action the {@link Runnable} that will execute + * @return a {@link Function} to be passed as an argument to + * {@link CompletionStage#thenCompose(Function)} + * @see CompletableFuture#thenCompose(Function) + */ + public static Function> run(Runnable action) + { + return val -> + { + CompletableFuture future = new CompletableFuture<>(); + runBlocking(() -> + { + action.run(); + future.complete(null); + }); + return future; + }; + } + + /** + * Transform a value contained within a {@link CompletionStage} + * by executing a mapping {@link Function} on the main thread. + * + * @param fn the {@link Function} used to transform the value + * @return a {@link Function} to be passed as an argument to + * {@link CompletionStage#thenCompose(Function)} + * @see CompletableFuture#thenCompose(Function) + */ + public static Function> map(Function fn) + { + return val -> + { + CompletableFuture future = new CompletableFuture<>(); + runBlocking(() -> future.complete(fn.apply(val))); + return future; + }; + } + + /** + * Create a {@link CompletionStage} from a supplier executed on the + * main thread. + * + * @param supplier the supplier to run on the main thread + * @return a {@link CompletionStage} whose value will be supplied + * during the next Minecraft tick + */ + public static CompletionStage supply(Supplier supplier) + { + CompletableFuture future = new CompletableFuture<>(); + runBlocking(() -> future.complete(supplier.get())); + return future; + } +} diff --git a/Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java b/Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java new file mode 100644 index 000000000..e0148bd0a --- /dev/null +++ b/Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java @@ -0,0 +1,291 @@ +package mineplex.core.database; + +import com.google.common.collect.ImmutableMap; +import mineplex.serverdata.database.DBPool; + +import java.sql.*; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * A SQL-backed repository supporting {@link String} keys and + * values of type {@link V} + *

+ * Each java primitive (sans char) and String are supported by default. + * Serializing functions for any additional types can be supplied + * to {@link PlayerKeyValueRepository(String, Serializer, Deserializer)}. + * For example, if {@link String} was not supported, one could use: + *

+ * {@code new PlayerKeyValueRepository("tableName", PreparedStatement::setString, ResultSet::getString, "VARCHAR(255)")} + *

+ * NOTE: EACH CONSTRUCTOR IS BLOCKING, and initializes a backing table + * if one does not yet exist + * + * @param The value type to use for this repository + */ +public class PlayerKeyValueRepository +{ + private static final ImmutableMap, ValueMapper> PRIM_MAPPERS = ImmutableMap., ValueMapper>builder() + .put(String.class, new ValueMapper<>(PreparedStatement::setString, ResultSet::getString, "VARCHAR(255)")) + .put(Boolean.class, new ValueMapper<>(PreparedStatement::setBoolean, ResultSet::getBoolean, "BOOL")) + .put(Byte.class, new ValueMapper<>(PreparedStatement::setByte, ResultSet::getByte, "TINYINT")) + .put(Short.class, new ValueMapper<>(PreparedStatement::setShort, ResultSet::getShort, "SMALLINT")) + .put(Integer.class, new ValueMapper<>(PreparedStatement::setInt, ResultSet::getInt, "INTEGER")) + .put(Long.class, new ValueMapper<>(PreparedStatement::setLong, ResultSet::getLong, "BIGINT")) + .put(Float.class, new ValueMapper<>(PreparedStatement::setFloat, ResultSet::getFloat, "REAL")) + .put(Double.class, new ValueMapper<>(PreparedStatement::setDouble, ResultSet::getDouble, "DOUBLE")) + .build(); + private final String _tableName; + private final ValueMapper _mapper; + + /** + * Build a PlayerKeyValueRepository with the given class' + * built-in deserializer. + * + * @param tableName the underlying table's name + * @param clazz the type of values to used + * @throws IllegalArgumentException if the provided class isn't a supported type + */ + @SuppressWarnings("unchecked") // java's generics are garbage. + public PlayerKeyValueRepository(String tableName, Class clazz) // we could infer the type parameter at runtime, but it's super ugly + { + this(tableName, (ValueMapper) PRIM_MAPPERS.get(clazz)); + } + + /** + * Build a PlayerKeyValueRepository with an explicit deserializer. + * This is the constructor to use if the type you're deserializing + * isn't supported by default. + * + * @param tableName the underlying table's name + * @param serializer the serializing function used to insert values + * @param deserializer the deserializing function used to retrieve + * values + * @param columnDef the value type's SQL datatype declaration, e.g., {@code "VARCHAR(255)"} for Strings. + */ + public PlayerKeyValueRepository(String tableName, Serializer serializer, Deserializer deserializer, String columnDef) + { + this(tableName, new ValueMapper(serializer, deserializer, columnDef)); + } + + private PlayerKeyValueRepository(String tableName, ValueMapper mapper) + { + this._tableName = tableName; + this._mapper = mapper; + + // Create a table to back this repository + try (Connection conn = DBPool.getAccount().getConnection()) + { + Statement stmt = conn.createStatement(); + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS " + _tableName + "(" + + "accountId INT NOT NULL," + + "kvKey VARCHAR(255) NOT NULL," + + "kvValue " + _mapper._columnDef + "," + + "PRIMARY KEY (accountId,kvKey)," + + "INDEX acc_ind (accountId)," + + "FOREIGN KEY (accountId) REFERENCES accounts(id) ON DELETE NO ACTION ON UPDATE NO ACTION" + + ")"); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + /** + * Get all value for a player's key + * + * @param uuid the {@link UUID} of the player + * @return a CompletableFuture containing all key/value pairs + * associated with the player + */ + public CompletableFuture get(UUID uuid, String key) + { + return CompletableFuture.supplyAsync(() -> + { + try (Connection conn = DBPool.getAccount().getConnection()) + { + PreparedStatement stmt = conn.prepareStatement("SELECT kvValue FROM " + _tableName + " WHERE accountId = (SELECT id FROM accounts WHERE uuid=?) AND kvKey=?"); + stmt.setString(1, uuid.toString()); + stmt.setString(2, key); + + ResultSet set = stmt.executeQuery(); + if (set.next()) + { + return _mapper._deserializer.read(set, 1); + } + return null; + } catch (SQLException ignored) {} + + return null; // yuck + }); + } + + /** + * Get all key/value pairs for a player + * + * @param uuid the {@link UUID} of the player + * @return a CompletableFuture containing all key/value pairs + * associated with the player + */ + public CompletableFuture> getAll(UUID uuid) + { + return CompletableFuture.supplyAsync(() -> + { + try (Connection conn = DBPool.getAccount().getConnection()) + { + PreparedStatement stmt = conn.prepareStatement("SELECT kvKey, kvValue FROM " + _tableName + " WHERE accountId = (SELECT id FROM accounts WHERE uuid=?)"); + stmt.setString(1, uuid.toString()); + + ResultSet set = stmt.executeQuery(); + Map results = new HashMap<>(); + while (set.next()) + { + results.put(set.getString(1), _mapper._deserializer.read(set, 2)); + } + return results; + } catch (SQLException ignored) {} + + return new HashMap<>(); // yuck + }); + } + + /** + * Insert a key/value pair for a player + * + * @param uuid the {@link UUID} of the player + * @param key the key to insert + * @param value the value to insert + * @return a {@link CompletableFuture} whose value indicates + * success or failure + */ + public CompletableFuture put(UUID uuid, String key, V value) + { + return CompletableFuture.supplyAsync(() -> + { + try (Connection conn = DBPool.getAccount().getConnection()) + { + PreparedStatement stmt = conn.prepareStatement("REPLACE INTO " + _tableName + " (accountId, kvKey, kvValue) SELECT accounts.id, ?, ? FROM accounts WHERE uuid=?"); + stmt.setString(1, key); + _mapper._serializer.write(stmt, 2, value); + stmt.setString(3, uuid.toString()); + stmt.executeUpdate(); + return true; + + } catch (SQLException ignored) {} + + return false; + }); + } + + /** + * Insert many key/value pairs for a player + * + * @param uuid the {@link UUID} of the player + * @param values the map whose entries will be inserted for the + * player + * @return a {@link CompletableFuture} whose value indicates + * success or failure + */ + public CompletableFuture putAll(UUID uuid, Map values) + { + return CompletableFuture.supplyAsync(() -> + { + try (Connection conn = DBPool.getAccount().getConnection()) + { + PreparedStatement stmt = conn.prepareStatement("REPLACE INTO " + _tableName + " (accountId, kvKey, kvValue) SELECT accounts.id, ?, ? FROM accounts WHERE uuid=?"); + stmt.setString(3, uuid.toString()); + + for (Map.Entry entry : values.entrySet()) + { + stmt.setString(1, entry.getKey()); + _mapper._serializer.write(stmt, 2, entry.getValue()); + stmt.addBatch(); + } + stmt.executeBatch(); + return true; + + } catch (SQLException ignored) {} + + return false; + }); + } + + /** + * Remove a key's value for a player + * + * @param uuid the {@link UUID} of the player + * @param key the key to remove + * @return a {@link CompletableFuture} whose value indicates + * success or failure + */ + public CompletableFuture remove(UUID uuid, String key) + { + return CompletableFuture.supplyAsync(() -> + { + try (Connection conn = DBPool.getAccount().getConnection()) + { + PreparedStatement stmt = conn.prepareStatement("DELETE FROM " + _tableName + " WHERE accountId=(SELECT id FROM accounts WHERE uuid=?) AND kvKey=?"); + stmt.setString(1, uuid.toString()); + stmt.setString(2, key); + stmt.executeUpdate(); + return true; + + } catch (SQLException ignored) {} + + return false; + }); + } + + /** + * Remove all key/value pairs for a player + * + * @param uuid the {@link UUID} of the player + * @return a {@link CompletableFuture} whose value indicates + * success or failure + */ + public CompletableFuture removeAll(UUID uuid) + { + return CompletableFuture.supplyAsync(() -> + { + try (Connection conn = DBPool.getAccount().getConnection()) + { + PreparedStatement stmt = conn.prepareStatement("DELETE FROM " + _tableName + " WHERE accountId=(SELECT id FROM accounts WHERE uuid=?)"); + stmt.setString(1, uuid.toString()); + stmt.executeUpdate(); + return true; + + } catch (SQLException ignored) {} + + return false; + }); + } + + private static class ValueMapper + { + private final Serializer _serializer; + private final Deserializer _deserializer; + private final String _columnDef; + + private ValueMapper(Serializer serializer, Deserializer deserializer, String columnDef) + { + _serializer = serializer; + _deserializer = deserializer; + _columnDef = columnDef; + } + } + + @FunctionalInterface + public interface Serializer + { + void write(PreparedStatement statement, int index, V value) throws SQLException; + } + + @FunctionalInterface + public interface Deserializer + { + V read(ResultSet resultSet, int index) throws SQLException; + } +}