Start working on storage providers.

This commit is contained in:
Len 2024-06-16 23:54:04 +02:00
parent 767d248ac8
commit 5ed4c32cc1
10 changed files with 572 additions and 0 deletions

View File

@ -238,4 +238,19 @@ public class Config {
FEED_BY_OTHER = config.getString("messages.command.feed.feed-by-other", FEED_BY_OTHER);
}
public static String MYSQL_IP = "localhost";
public static String MYSQL_PORT = "3306";
public static String MYSQL_DATABASE_NAME = "essentia";
public static String MYSQL_USERNAME = "root";
public static String MYSQL_PASSWORD = "root";
public static int MYSQL_CONNECTIONS = 10;
public static int MYSQL_QUEUE_DELAY = 5;
private static void storage() {
MYSQL_IP = config.getString("storage.mysql.ip", MYSQL_IP);
MYSQL_PORT = config.getString("storage.mysql.port", MYSQL_PORT);
MYSQL_DATABASE_NAME = config.getString("storage.mysql.database", MYSQL_DATABASE_NAME);
MYSQL_USERNAME = config.getString("storage.mysql.username", MYSQL_USERNAME);
MYSQL_PASSWORD = config.getString("storage.mysql.password", MYSQL_PASSWORD);
}
}

View File

@ -0,0 +1,26 @@
package com.alttd.essentia.storage;
import com.alttd.essentia.EssentiaPlugin;
import com.alttd.essentia.storage.mysql.SQLStorageProvider;
import com.alttd.essentia.storage.sqlite.SQLiteStorageProvider;
import com.alttd.essentia.storage.yaml.YamlStorageProvider;
import java.io.File;
public abstract class StorageManager {
protected final EssentiaPlugin plugin;
public StorageManager(EssentiaPlugin plugin) {
this.plugin = plugin;
}
public StorageProvider storageProvider(StorageType type) {
return switch (type) {
case MYSQL -> new SQLStorageProvider(plugin);
case YAML -> new YamlStorageProvider(plugin, plugin.getDataFolder().getPath() + File.separator + "PlayerData");
case SQLITE -> throw new UnsupportedOperationException(); // TODO
};
}
}

View File

@ -0,0 +1,32 @@
package com.alttd.essentia.storage;
import com.alttd.essentia.EssentiaPlugin;
import com.alttd.essentia.user.EssentiaUser;
import com.alttd.essentia.user.User;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public abstract class StorageProvider {
protected final EssentiaPlugin plugin;
public StorageProvider(EssentiaPlugin plugin) {
this.plugin = plugin;
}
public EssentiaUser loadUser(UUID uuid) {
EssentiaUser user = load(uuid);
plugin.userManager().addUser(user);
// TODO -- UserLoadEvent?
return user;
}
protected abstract EssentiaUser load(UUID uuid);
public abstract void save(@NotNull User user) throws Exception;
public abstract void delete(UUID uuid) throws Exception;
}

View File

@ -0,0 +1,7 @@
package com.alttd.essentia.storage;
public enum StorageType {
YAML,
MYSQL,
SQLITE
}

View File

@ -0,0 +1,86 @@
package com.alttd.essentia.storage.mysql;
import com.alttd.essentia.EssentiaPlugin;
import com.alttd.essentia.configuration.Config;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection implements AutoCloseable {
private Connection connection;
private volatile boolean isActive;
protected final EssentiaPlugin plugin;
public DatabaseConnection(EssentiaPlugin plugin) {
this.plugin = plugin;
try {
openConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
private synchronized void openConnection() throws SQLException {
if (connection != null && !connection.isClosed()) {
return;
}
synchronized (this) {
if (connection != null && !connection.isClosed()) {
return;
}
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
connection = DriverManager.getConnection(
"jdbc:mysql://" + Config.MYSQL_IP + ":" + Config.MYSQL_PORT + "/" + Config.MYSQL_DATABASE_NAME +
"?autoReconnect=true&useSSL=false",
Config.MYSQL_USERNAME, Config.MYSQL_PASSWORD);
}
}
public synchronized Connection get() {
try {
openConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
public synchronized boolean isValid() {
try {
return !connection.isClosed() && connection.isValid(8000);
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
synchronized void setActive(boolean active) {
isActive = active;
}
public synchronized boolean isActive() {
return isActive;
}
@Override
public synchronized void close() {
try {
if (!connection.isClosed()) {
if (!connection.getAutoCommit()) {
connection.commit();
}
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,44 @@
package com.alttd.essentia.storage.mysql;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DatabaseQuery {
private final String statement;
private final DatabaseTask databaseTask;
public DatabaseQuery(String statement, DatabaseTask databaseTask) {
this.statement = statement;
this.databaseTask = databaseTask;
}
public DatabaseQuery(String statement) {
this(statement, ps -> {});
}
public ResultSet execute(Connection connection) {
try (PreparedStatement preparedStatement = connection.prepareStatement(statement)) {
databaseTask.edit(preparedStatement);
ResultSet resultSet = preparedStatement.executeQuery();
databaseTask.onSuccess(resultSet);
return resultSet;
} catch (SQLException e) {
databaseTask.onFailure(e);
}
return null;
}
public interface DatabaseTask {
void edit(PreparedStatement preparedStatement) throws SQLException;
default void onSuccess(ResultSet resultSet) throws SQLException {};
default void onFailure(SQLException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,58 @@
package com.alttd.essentia.storage.mysql;
import lombok.Getter;
import org.bukkit.scheduler.BukkitRunnable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
public class DatabaseQueue extends BukkitRunnable {
private final SQLStorageProvider sqlStorageProvider;
public DatabaseQueue(SQLStorageProvider sqlStorageProvider) {
this.sqlStorageProvider = sqlStorageProvider;
}
@Getter
public final Queue<DatabaseQuery> databaseQueryQueue = new LinkedBlockingQueue<>();
@Override
public void run() {
runTaskQueue();
}
public synchronized void runTaskQueue() {
if (databaseQueryQueue.isEmpty())
return;
DatabaseConnection databaseConnection = sqlStorageProvider.getDatabaseConnection();
Connection connection = databaseConnection.get();
try {
databaseConnection.setActive(true);
connection.setAutoCommit(false);
while (!databaseQueryQueue.isEmpty()) {
if (!databaseConnection.isValid())
return;
DatabaseQuery databaseQuery = databaseQueryQueue.poll();
if (databaseQuery == null)
return;
databaseQuery.execute(connection);
}
if (!connection.getAutoCommit()) {
connection.commit();
connection.setAutoCommit(true);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
databaseConnection.setActive(false);
}
}
}

View File

@ -0,0 +1,161 @@
package com.alttd.essentia.storage.mysql;
import com.alttd.essentia.EssentiaPlugin;
import com.alttd.essentia.configuration.Config;
import com.alttd.essentia.storage.StorageProvider;
import com.alttd.essentia.user.EssentiaUser;
import com.alttd.essentia.user.User;
import org.jetbrains.annotations.NotNull;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class SQLStorageProvider extends StorageProvider {
private final DatabaseQueue databaseQueue;
private final List<DatabaseConnection> CONNECTIONPOOL = new ArrayList<>();
public SQLStorageProvider(EssentiaPlugin plugin) {
super(plugin);
databaseQueue = new DatabaseQueue(this);
int delay = Config.MYSQL_QUEUE_DELAY * 20;
databaseQueue.runTaskTimerAsynchronously(plugin, delay, delay);
// preload out database connections, TODO FIND A BETTER WAY TO LIMIT THIS
for (int i = 1; i < Config.MYSQL_CONNECTIONS; i++) {
CONNECTIONPOOL.add(null);
}
createTables();
}
private void createTables() {
// TODO -- create table
String userTable = "CREATE TABLE IF NOT EXISTS users(" +
"id VARCHAR(36) NOT NULL, " +
"PRIMARY KEY (id)" +
")";
addDatabaseQuery(new DatabaseQuery(userTable), false);
}
public DatabaseConnection getDatabaseConnection() {
for (int i = 0; i < Config.MYSQL_CONNECTIONS; i++) {
DatabaseConnection connection = CONNECTIONPOOL.get(i);
if (connection == null) {
return generateDatabaseConnection(i);
} else if (!connection.isActive()) {
if (connection.isValid()) {
return connection;
} else {
connection.close();
return generateDatabaseConnection(i);
}
}
}
// This will cause an infinite running loop, throw an exception or wait for a connection to be available?
return getDatabaseConnection();
}
private DatabaseConnection generateDatabaseConnection(int index) {
DatabaseConnection connection = new DatabaseConnection(plugin);
CONNECTIONPOOL.set(index, connection);
return connection;
}
private void closeDatabaseConnections() {
for (DatabaseConnection connection : CONNECTIONPOOL) {
if (connection == null || connection.isValid())
continue;
if (!connection.isActive()) {
connection.close();
} else {
while (connection.isActive()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// This should not be interrupted as this is saving all the shops in the background for us.
e.printStackTrace();
}
}
connection.close();
}
}
}
public void unload() {
if (databaseQueue != null && !databaseQueue.isCancelled()) {
databaseQueue.cancel();
databaseQueue.runTaskQueue();
}
closeDatabaseConnections();
}
public void addDatabaseQuery(DatabaseQuery databaseQuery, boolean queue) {
if (queue) {
databaseQueue.databaseQueryQueue().offer(databaseQuery);
} else {
databaseQuery.execute(getDatabaseConnection().get());
}
}
boolean hasTable(String table) {
DatabaseConnection connection = getDatabaseConnection();
boolean match = false;
try (ResultSet rs = connection.get().getMetaData().getTables(null, null, table, null)) {
while (rs.next()) {
if (table.equalsIgnoreCase(rs.getString("TABLE_NAME"))) {
match = true;
break;
}
}
} catch (SQLException e) {
return match;
}
return match;
}
@Override
protected EssentiaUser load(UUID uuid) {
String sql = "SELECT * FROM users WHERE uuid = ?";
DatabaseQuery databaseQuery = new DatabaseQuery(sql, ps -> ps.setString(1, uuid.toString()));
try (ResultSet resultSet = databaseQuery.execute(getDatabaseConnection().get())) {
if (!resultSet.next()) {
return null; // user is not in the db
}
return new EssentiaUser.Builder()
.uuid(UUID.fromString(resultSet.getString("id")))
.build();
} catch (SQLException e) {
// catch this nicely
}
return null;
}
@Override
public void save(@NotNull User user) throws Exception {
String sql = "INSERT INTO users WHERE uuid = ?"; // upsert query
addDatabaseQuery(
new DatabaseQuery(sql, ps -> ps.setString(1, user.getUUID().toString())), true
);
}
@Override
public void delete(UUID uuid) throws Exception {
String sql = "DELETE FROM users WHERE uuid = ?";
addDatabaseQuery(
new DatabaseQuery(sql, new DatabaseQuery.DatabaseTask() {
@Override
public void edit(PreparedStatement ps) throws SQLException {
ps.setString(1, uuid.toString());
}
}), true
);
}
}

View File

@ -0,0 +1,32 @@
package com.alttd.essentia.storage.sqlite;
import com.alttd.essentia.EssentiaPlugin;
import com.alttd.essentia.storage.StorageProvider;
import com.alttd.essentia.user.EssentiaUser;
import com.alttd.essentia.user.User;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
// TODO -- add support for SQLite
public class SQLiteStorageProvider extends StorageProvider {
public SQLiteStorageProvider(EssentiaPlugin plugin) {
super(plugin);
}
@Override
protected EssentiaUser load(UUID uuid) {
return null;
}
@Override
public void save(@NotNull User user) throws Exception {
}
@Override
public void delete(UUID uuid) throws Exception {
}
}

View File

@ -0,0 +1,111 @@
package com.alttd.essentia.storage.yaml;
import com.alttd.essentia.EssentiaPlugin;
import com.alttd.essentia.storage.StorageProvider;
import com.alttd.essentia.user.EssentiaUser;
import com.alttd.essentia.user.User;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
// TODO -- switch to configurate?
public class YamlStorageProvider extends StorageProvider {
private final String dataDirectory;
public YamlStorageProvider(EssentiaPlugin plugin, String dataDirectory) {
super(plugin);
this.dataDirectory = dataDirectory;
}
@Override
protected EssentiaUser load(UUID uuid) {
File configFile = new File(dataDirectory, uuid + ".yml");
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
return new EssentiaUser.Builder()
.uuid(uuid)
.backLocation(getStoredLocation(config,"teleports.back"))
.deathLocation(getStoredLocation(config,"teleports.death"))
.homes(getHomeData(config))
.allowTeleports(config.getBoolean("allow-teleports", true))
.build();
}
@Override
public void save(@NotNull User user) throws Exception {
if (user.saving()) return;
user.saving(true);
File configFile = new File(dataDirectory, user.getUUID() + ".yml");
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
setStoredLocation(config, "teleports.back", user.getBackLocation(false));
setStoredLocation(config, "teleports.death", user.getBackLocation(true));
for (Map.Entry<String, Location> entry : user.getHomeData().entrySet()) {
setStoredLocation(config, "home." + entry.getKey(), entry.getValue());
}
config.set("allow-teleports", user.allowTeleports());
config.save(configFile);
user.saving(false);
}
@Override
public void delete(UUID uuid) throws Exception {
Path path = Path.of(dataDirectory, uuid.toString() + ".yml");
Files.deleteIfExists(path);
}
void setStoredLocation(YamlConfiguration config, String path, Location location) {
if (location == null) {
config.set(path, null);
return;
}
config.set(path + ".world", location.getWorld().getName());
config.set(path + ".x", location.getX());
config.set(path + ".y", location.getY());
config.set(path + ".z", location.getZ());
config.set(path + ".pitch", location.getPitch());
config.set(path + ".yaw", location.getYaw());
}
Location getStoredLocation(YamlConfiguration config, String path) {
if (config.get(path) == null) {
return null;
}
World world = Bukkit.getWorld(config.getString(path + ".world", ""));
if (world == null) {
return null;
}
double x = config.getDouble(path + ".x");
double y = config.getDouble(path + ".y");
double z = config.getDouble(path + ".z");
float pitch = (float) config.getDouble(path + ".pitch");
float yaw = (float) config.getDouble(path + ".yaw");
return new Location(world, x, y, z, yaw, pitch);
}
public Map<String, Location> getHomeData(YamlConfiguration config) {
ConfigurationSection section = config.getConfigurationSection("home");
if (section == null) {
return null;
}
Map<String, Location> map = new HashMap<>();
for (String key : section.getValues(false).keySet()) {
map.put(key, getStoredLocation(config, "home." + key));
}
return map;
}
}