From 5ed4c32cc1430e4dce3eb1ef467d27eee5326d6f Mon Sep 17 00:00:00 2001 From: Len <40720638+destro174@users.noreply.github.com> Date: Sun, 16 Jun 2024 23:54:04 +0200 Subject: [PATCH] Start working on storage providers. --- .../alttd/essentia/configuration/Config.java | 15 ++ .../essentia/storage/StorageManager.java | 26 +++ .../essentia/storage/StorageProvider.java | 32 ++++ .../alttd/essentia/storage/StorageType.java | 7 + .../storage/mysql/DatabaseConnection.java | 86 ++++++++++ .../essentia/storage/mysql/DatabaseQuery.java | 44 +++++ .../essentia/storage/mysql/DatabaseQueue.java | 58 +++++++ .../storage/mysql/SQLStorageProvider.java | 161 ++++++++++++++++++ .../storage/sqlite/SQLiteStorageProvider.java | 32 ++++ .../storage/yaml/YamlStorageProvider.java | 111 ++++++++++++ 10 files changed, 572 insertions(+) create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/StorageManager.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/StorageProvider.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/StorageType.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseConnection.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQuery.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQueue.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/mysql/SQLStorageProvider.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/sqlite/SQLiteStorageProvider.java create mode 100644 plugin/src/main/java/com/alttd/essentia/storage/yaml/YamlStorageProvider.java diff --git a/plugin/src/main/java/com/alttd/essentia/configuration/Config.java b/plugin/src/main/java/com/alttd/essentia/configuration/Config.java index 2a8c79e..df4011c 100755 --- a/plugin/src/main/java/com/alttd/essentia/configuration/Config.java +++ b/plugin/src/main/java/com/alttd/essentia/configuration/Config.java @@ -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); + } + } diff --git a/plugin/src/main/java/com/alttd/essentia/storage/StorageManager.java b/plugin/src/main/java/com/alttd/essentia/storage/StorageManager.java new file mode 100644 index 0000000..62ededd --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/StorageManager.java @@ -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 + }; + } + +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/StorageProvider.java b/plugin/src/main/java/com/alttd/essentia/storage/StorageProvider.java new file mode 100644 index 0000000..9efa5d5 --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/StorageProvider.java @@ -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; +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/StorageType.java b/plugin/src/main/java/com/alttd/essentia/storage/StorageType.java new file mode 100644 index 0000000..ad910b5 --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/StorageType.java @@ -0,0 +1,7 @@ +package com.alttd.essentia.storage; + +public enum StorageType { + YAML, + MYSQL, + SQLITE +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseConnection.java b/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseConnection.java new file mode 100644 index 0000000..fffc719 --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseConnection.java @@ -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(); + } + } +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQuery.java b/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQuery.java new file mode 100644 index 0000000..b55bc79 --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQuery.java @@ -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(); + } + } +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQueue.java b/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQueue.java new file mode 100644 index 0000000..41cef6d --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/mysql/DatabaseQueue.java @@ -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 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); + } + + } +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/mysql/SQLStorageProvider.java b/plugin/src/main/java/com/alttd/essentia/storage/mysql/SQLStorageProvider.java new file mode 100644 index 0000000..eda0d87 --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/mysql/SQLStorageProvider.java @@ -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 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 + ); + } + +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/sqlite/SQLiteStorageProvider.java b/plugin/src/main/java/com/alttd/essentia/storage/sqlite/SQLiteStorageProvider.java new file mode 100644 index 0000000..5cc152a --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/sqlite/SQLiteStorageProvider.java @@ -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 { + + } + +} diff --git a/plugin/src/main/java/com/alttd/essentia/storage/yaml/YamlStorageProvider.java b/plugin/src/main/java/com/alttd/essentia/storage/yaml/YamlStorageProvider.java new file mode 100644 index 0000000..7e510f8 --- /dev/null +++ b/plugin/src/main/java/com/alttd/essentia/storage/yaml/YamlStorageProvider.java @@ -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 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 getHomeData(YamlConfiguration config) { + ConfigurationSection section = config.getConfigurationSection("home"); + if (section == null) { + return null; + } + Map map = new HashMap<>(); + for (String key : section.getValues(false).keySet()) { + map.put(key, getStoredLocation(config, "home." + key)); + } + + return map; + } + +}