Add PlayerKeyValueRepository and BukkitFuture

PlayerKeyValueRepository<V> 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<String> 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<String,String>`
    }));
This commit is contained in:
cnr 2016-05-24 19:18:40 -05:00
parent 9da9dce398
commit 857cf6ad30
2 changed files with 402 additions and 0 deletions

View File

@ -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.
* <p>
* 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 <T> Function<T, CompletionStage<Void>> accept(Consumer<? super T> action)
{
return val ->
{
CompletableFuture<Void> 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 <T> Function<T, CompletionStage<Void>> run(Runnable action)
{
return val ->
{
CompletableFuture<Void> 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 <T,U> Function<T, CompletionStage<U>> map(Function<? super T,? extends U> fn)
{
return val ->
{
CompletableFuture<U> 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 <T> CompletionStage<T> supply(Supplier<T> supplier)
{
CompletableFuture<T> future = new CompletableFuture<>();
runBlocking(() -> future.complete(supplier.get()));
return future;
}
}

View File

@ -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}
* <p>
* 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:
* <p>
* {@code new PlayerKeyValueRepository("tableName", PreparedStatement::setString, ResultSet::getString, "VARCHAR(255)")}
* <p>
* NOTE: EACH CONSTRUCTOR IS BLOCKING, and initializes a backing table
* if one does not yet exist
*
* @param <V> The value type to use for this repository
*/
public class PlayerKeyValueRepository<V>
{
private static final ImmutableMap<Class<?>, ValueMapper<?>> PRIM_MAPPERS = ImmutableMap.<Class<?>, 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<V> _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<V> clazz) // we could infer the type parameter at runtime, but it's super ugly
{
this(tableName, (ValueMapper<V>) 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<V> serializer, Deserializer<V> deserializer, String columnDef)
{
this(tableName, new ValueMapper<V>(serializer, deserializer, columnDef));
}
private PlayerKeyValueRepository(String tableName, ValueMapper<V> 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<V> 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<Map<String,V>> 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<String, V> 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<Boolean> 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<Boolean> putAll(UUID uuid, Map<String,V> 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<String, V> 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<Boolean> 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<Boolean> 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<V>
{
private final Serializer<V> _serializer;
private final Deserializer<V> _deserializer;
private final String _columnDef;
private ValueMapper(Serializer<V> serializer, Deserializer<V> deserializer, String columnDef)
{
_serializer = serializer;
_deserializer = deserializer;
_columnDef = columnDef;
}
}
@FunctionalInterface
public interface Serializer<V>
{
void write(PreparedStatement statement, int index, V value) throws SQLException;
}
@FunctionalInterface
public interface Deserializer<V>
{
V read(ResultSet resultSet, int index) throws SQLException;
}
}