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; + } +}