diff --git a/api/src/main/java/com/infernalsuite/asp/api/loaders/SlimeLoader.java b/api/src/main/java/com/infernalsuite/asp/api/loaders/SlimeLoader.java index 6cc1ff27..5e2b4b02 100644 --- a/api/src/main/java/com/infernalsuite/asp/api/loaders/SlimeLoader.java +++ b/api/src/main/java/com/infernalsuite/asp/api/loaders/SlimeLoader.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; /** * SlimeLoaders are in charge of loading worlds @@ -50,6 +51,15 @@ public interface SlimeLoader { */ void saveWorld(String worldName, byte[] serializedWorld) throws IOException; + /** + * Saves multiple worlds at once. This method will also + * lock the worlds, in case they're not locked already. + * + * @param worlds a map containing the world names as keys and their data files as values + * @throws IOException if the worlds could not be saved. + */ + void saveWorlds(Map worlds) throws IOException; + /** * Deletes a world from the data source. * diff --git a/loaders/api-loader/src/main/java/com/infernalsuite/asp/loaders/api/APILoader.java b/loaders/api-loader/src/main/java/com/infernalsuite/asp/loaders/api/APILoader.java index 10e7b6ec..67596194 100644 --- a/loaders/api-loader/src/main/java/com/infernalsuite/asp/loaders/api/APILoader.java +++ b/loaders/api-loader/src/main/java/com/infernalsuite/asp/loaders/api/APILoader.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.Base64; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -174,6 +175,11 @@ public void saveWorld(String worldName, byte[] serializedWorld) throws IOExcepti this.logger.warn("Illegal call to saveWorld: API Worlds cannot be saved. They're always read-only."); } + @Override + public void saveWorlds(Map worlds) throws IOException { + this.logger.warn("Illegal call to saveWorld: API Worlds cannot be saved. They're always read-only."); + } + @Override public void deleteWorld(String worldName) { this.logger.warn("Illegal call to deleteWorld: API Worlds cannot be deleted through the loader."); diff --git a/loaders/build.gradle.kts b/loaders/build.gradle.kts index 73dc4375..7eb6926e 100644 --- a/loaders/build.gradle.kts +++ b/loaders/build.gradle.kts @@ -9,7 +9,7 @@ dependencies { api(project(":loaders:api-loader")) api(project(":loaders:file-loader")) api(project(":loaders:mongo-loader")) - api(project(":loaders:mysql-loader")) + api(project(":loaders:sql-loader")) api(project(":loaders:redis-loader")) compileOnly(paperApi()) diff --git a/loaders/file-loader/src/main/java/com/infernalsuite/asp/loaders/file/FileLoader.java b/loaders/file-loader/src/main/java/com/infernalsuite/asp/loaders/file/FileLoader.java index 3fa87292..0ee09271 100644 --- a/loaders/file-loader/src/main/java/com/infernalsuite/asp/loaders/file/FileLoader.java +++ b/loaders/file-loader/src/main/java/com/infernalsuite/asp/loaders/file/FileLoader.java @@ -9,6 +9,7 @@ import java.nio.file.NotDirectoryException; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class FileLoader implements SlimeLoader { @@ -63,6 +64,16 @@ public void saveWorld(String worldName, byte[] serializedWorld) throws IOExcepti } } + @Override + public void saveWorlds(Map worlds) throws IOException { + for (Map.Entry entry : worlds.entrySet()) { + File file = new File(worldDir, entry.getKey() + ".slime"); + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(entry.getValue()); + } + } + } + @Override public void deleteWorld(String worldName) throws UnknownWorldException, IOException { if (!worldExists(worldName)) { diff --git a/loaders/mongo-loader/build.gradle.kts b/loaders/mongo-loader/build.gradle.kts index c3861aed..f58d740a 100644 --- a/loaders/mongo-loader/build.gradle.kts +++ b/loaders/mongo-loader/build.gradle.kts @@ -7,7 +7,7 @@ dependencies { compileOnly(project(":api")) compileOnly(paperApi()) - api(libs.mongo) + compileOnlyApi(libs.mongo) } publishConfiguration { diff --git a/loaders/mongo-loader/src/main/java/com/infernalsuite/asp/loaders/mongo/MongoLoader.java b/loaders/mongo-loader/src/main/java/com/infernalsuite/asp/loaders/mongo/MongoLoader.java index 533ffbda..f3964f98 100644 --- a/loaders/mongo-loader/src/main/java/com/infernalsuite/asp/loaders/mongo/MongoLoader.java +++ b/loaders/mongo-loader/src/main/java/com/infernalsuite/asp/loaders/mongo/MongoLoader.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class MongoLoader extends UpdatableLoader { @@ -179,6 +180,35 @@ public void saveWorld(String worldName, byte[] serializedWorld) throws IOExcepti } } + @Override + public void saveWorlds(Map worlds) throws IOException { + MongoDatabase mongoDatabase = client.getDatabase(database); + GridFSBucket bucket = GridFSBuckets.create(mongoDatabase, collection); + MongoCollection mongoCollection = mongoDatabase.getCollection(collection); + + List> bulkOps = new ArrayList<>(); + + for (Map.Entry entry : worlds.entrySet()) { + String worldName = entry.getKey(); + byte[] data = entry.getValue(); + + GridFSFile oldFile = bucket.find(Filters.eq("filename", worldName)).first(); + if (oldFile != null) { + bucket.delete(oldFile.getObjectId()); + } + + bucket.uploadFromStream(worldName, new ByteArrayInputStream(data)); + + Bson query = Filters.eq("name", worldName); + Bson update = Updates.set("name", worldName); + bulkOps.add(new UpdateOneModel<>(query, update, new UpdateOptions().upsert(true))); + } + + if (!bulkOps.isEmpty()) { + mongoCollection.bulkWrite(bulkOps); + } + } + @Override public void deleteWorld(String worldName) throws IOException, UnknownWorldException { try { @@ -203,5 +233,4 @@ public void deleteWorld(String worldName) throws IOException, UnknownWorldExcept throw new IOException(ex); } } - } diff --git a/loaders/mysql-loader/src/main/java/com/infernalsuite/asp/loaders/mysql/MysqlLoader.java b/loaders/mysql-loader/src/main/java/com/infernalsuite/asp/loaders/mysql/MysqlLoader.java deleted file mode 100644 index 546a42ce..00000000 --- a/loaders/mysql-loader/src/main/java/com/infernalsuite/asp/loaders/mysql/MysqlLoader.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.infernalsuite.asp.loaders.mysql; - -import com.infernalsuite.asp.api.exceptions.UnknownWorldException; -import com.infernalsuite.asp.api.loaders.UpdatableLoader; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.jetbrains.annotations.ApiStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -public class MysqlLoader extends UpdatableLoader { - - private static final Logger LOGGER = LoggerFactory.getLogger(MysqlLoader.class); - - private static final int CURRENT_DB_VERSION = 1; - - // Database version handling queries - private static final String CREATE_VERSIONING_TABLE_QUERY = "CREATE TABLE IF NOT EXISTS `database_version` (`id` INT NOT NULL AUTO_INCREMENT, " + - "`version` INT(11), PRIMARY KEY(id));"; - private static final String INSERT_VERSION_QUERY = "INSERT INTO `database_version` (`id`, `version`) VALUES (1, ?) ON DUPLICATE KEY UPDATE `id` = ?;"; - private static final String GET_VERSION_QUERY = "SELECT `version` FROM `database_version` WHERE `id` = 1;"; - - // v1 update query - private static final String ALTER_LOCKED_COLUMN_QUERY = "ALTER TABLE `worlds` CHANGE COLUMN `locked` `locked` BIGINT NOT NULL DEFAULT 0;"; - - // World handling queries - private static final String CREATE_WORLDS_TABLE_QUERY = "CREATE TABLE IF NOT EXISTS `worlds` (`id` INT NOT NULL AUTO_INCREMENT, " + - "`name` VARCHAR(255) UNIQUE, `locked` BIGINT, `world` MEDIUMBLOB, PRIMARY KEY(id));"; - private static final String SELECT_WORLD_QUERY = "SELECT `world` FROM `worlds` WHERE `name` = ?;"; - private static final String UPDATE_WORLD_QUERY = "INSERT INTO `worlds` (`name`, `world`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `world` = ?;"; - private static final String DELETE_WORLD_QUERY = "DELETE FROM `worlds` WHERE `name` = ?;"; - private static final String LIST_WORLDS_QUERY = "SELECT `name` FROM `worlds`;"; - - private final HikariDataSource source; - - public MysqlLoader(String sqlURL, String host, int port, String database, boolean useSSL, String username, String password) throws SQLException { - HikariConfig hikariConfig = new HikariConfig(); - - sqlURL = sqlURL.replace("{host}", host); - sqlURL = sqlURL.replace("{port}", String.valueOf(port)); - sqlURL = sqlURL.replace("{database}", database); - sqlURL = sqlURL.replace("{usessl}", String.valueOf(useSSL)); - - hikariConfig.setJdbcUrl(sqlURL); -// hikariConfig.setJdbcUrl("jdbc:mysql://" + config.getHost() + ":" + config.getPort() + "/" + config.getDatabase() + "?autoReconnect=true&allowMultiQueries=true&useSSL=" + config.isUsessl()); - hikariConfig.setUsername(username); - hikariConfig.setPassword(password); - - hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); - hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); - hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - hikariConfig.addDataSourceProperty("useServerPrepStmts", "true"); - hikariConfig.addDataSourceProperty("useLocalSessionState", "true"); - hikariConfig.addDataSourceProperty("rewriteBatchedStatements", "true"); - hikariConfig.addDataSourceProperty("cacheResultSetMetadata", "true"); - hikariConfig.addDataSourceProperty("cacheServerConfiguration", "true"); - hikariConfig.addDataSourceProperty("elideSetAutoCommits", "true"); - hikariConfig.addDataSourceProperty("maintainTimeStats", "false"); - - source = new HikariDataSource(hikariConfig); - init(); - } - - @ApiStatus.Experimental - public MysqlLoader(HikariDataSource hikariDataSource) throws SQLException { - source = hikariDataSource; - init(); - } - - @Override - public void update() throws IOException, NewerStorageException { - try (Connection con = source.getConnection()) { - int version; - - try (PreparedStatement statement = con.prepareStatement(GET_VERSION_QUERY); - ResultSet set = statement.executeQuery()) { - version = set.next() ? set.getInt(1) : -1; - } - - if (version > CURRENT_DB_VERSION) { - throw new NewerStorageException(CURRENT_DB_VERSION, version); - } - - if (version < CURRENT_DB_VERSION) { - LOGGER.warn("Your SWM MySQL database is outdated. The update process will start in 10 seconds."); - LOGGER.warn("Note that this update might make your database incompatible with older SWM versions."); - LOGGER.warn("Make sure no other servers with older SWM versions are using this database."); - LOGGER.warn("Shut down the server to prevent your database from being updated."); - - try { - Thread.sleep(10000L); - } catch (InterruptedException ignored) { - LOGGER.info("Update process aborted."); - return; - } - - // Update to v1: alter locked column to store a long - try (PreparedStatement statement = con.prepareStatement(ALTER_LOCKED_COLUMN_QUERY)) { - statement.executeUpdate(); - } - - // Insert/update database version table - try (PreparedStatement statement = con.prepareStatement(INSERT_VERSION_QUERY)) { - statement.setInt(1, CURRENT_DB_VERSION); - statement.setInt(2, CURRENT_DB_VERSION); - statement.executeUpdate(); - } - } - } catch (SQLException ex) { - throw new IOException(ex); - } - } - - @Override - public byte[] readWorld(String worldName) throws UnknownWorldException, IOException { - try (Connection con = source.getConnection(); - PreparedStatement statement = con.prepareStatement(SELECT_WORLD_QUERY)) { - statement.setString(1, worldName); - ResultSet set = statement.executeQuery(); - - if (!set.next()) { - throw new UnknownWorldException(worldName); - } - - return set.getBytes("world"); - } catch (SQLException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean worldExists(String worldName) throws IOException { - try (Connection con = source.getConnection(); - PreparedStatement statement = con.prepareStatement(SELECT_WORLD_QUERY)) { - statement.setString(1, worldName); - ResultSet set = statement.executeQuery(); - - return set.next(); - } catch (SQLException ex) { - throw new IOException(ex); - } - } - - @Override - public List listWorlds() throws IOException { - List worldList = new ArrayList<>(); - - try (Connection con = source.getConnection(); - PreparedStatement statement = con.prepareStatement(LIST_WORLDS_QUERY)) { - ResultSet set = statement.executeQuery(); - - while (set.next()) { - worldList.add(set.getString("name")); - } - } catch (SQLException ex) { - throw new IOException(ex); - } - - return worldList; - } - - @Override - public void saveWorld(String worldName, byte[] serializedWorld) throws IOException { - try (Connection con = source.getConnection(); - PreparedStatement statement = con.prepareStatement(UPDATE_WORLD_QUERY)) { - statement.setString(1, worldName); - statement.setBytes(2, serializedWorld); - statement.setBytes(3, serializedWorld); - statement.executeUpdate(); - - } catch (SQLException ex) { - throw new IOException(ex); - } - } - - @Override - public void deleteWorld(String worldName) throws IOException, UnknownWorldException { - try (Connection con = source.getConnection(); - PreparedStatement statement = con.prepareStatement(DELETE_WORLD_QUERY)) { - statement.setString(1, worldName); - - if (statement.executeUpdate() == 0) { - throw new UnknownWorldException(worldName); - } - } catch (SQLException ex) { - throw new IOException(ex); - } - } - - private void init() throws SQLException { - try (Connection con = source.getConnection()) { - // Create worlds table - try (PreparedStatement statement = con.prepareStatement(CREATE_WORLDS_TABLE_QUERY)) { - statement.execute(); - } - - // Create versioning table - try (PreparedStatement statement = con.prepareStatement(CREATE_VERSIONING_TABLE_QUERY)) { - statement.execute(); - } - } - } - -} diff --git a/loaders/redis-loader/build.gradle.kts b/loaders/redis-loader/build.gradle.kts index b41810de..9cd4bc02 100644 --- a/loaders/redis-loader/build.gradle.kts +++ b/loaders/redis-loader/build.gradle.kts @@ -6,7 +6,7 @@ plugins { dependencies { compileOnly(project(":api")) - api(libs.lettuce) + compileOnlyApi(libs.lettuce) compileOnly(paperApi()) } diff --git a/loaders/redis-loader/src/main/java/com/infernalsuite/asp/loaders/redis/RedisLoader.java b/loaders/redis-loader/src/main/java/com/infernalsuite/asp/loaders/redis/RedisLoader.java index b4f6c0d9..d7840ccd 100644 --- a/loaders/redis-loader/src/main/java/com/infernalsuite/asp/loaders/redis/RedisLoader.java +++ b/loaders/redis-loader/src/main/java/com/infernalsuite/asp/loaders/redis/RedisLoader.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class RedisLoader implements SlimeLoader { @@ -55,6 +56,23 @@ public void saveWorld(String worldName, byte[] bytes) throws IOException { connection.sadd(WORLD_LIST_PREFIX, worldName.getBytes(StandardCharsets.UTF_8)); } + public void saveWorlds(Map worlds) throws IOException { + if (worlds.isEmpty()) { + return; + } + + try { + connection.mset(worlds); + byte[][] names = worlds.keySet().stream() + .map(str -> str.getBytes(StandardCharsets.UTF_8)) + .toArray(byte[][]::new); + + connection.sadd(WORLD_LIST_PREFIX, names); + } catch (Exception e) { + throw new IOException(e); + } + } + @Override public void deleteWorld(String worldName) throws UnknownWorldException, IOException { long deletedCount = connection.del(WORLD_DATA_PREFIX + worldName); diff --git a/loaders/mysql-loader/build.gradle.kts b/loaders/sql-loader/build.gradle.kts similarity index 58% rename from loaders/mysql-loader/build.gradle.kts rename to loaders/sql-loader/build.gradle.kts index 03df1323..394066d3 100644 --- a/loaders/mysql-loader/build.gradle.kts +++ b/loaders/sql-loader/build.gradle.kts @@ -6,11 +6,11 @@ plugins { dependencies { compileOnly(project(":api")) - api(libs.hikari) + compileOnlyApi(libs.hikari) compileOnly(paperApi()) } publishConfiguration { - name = "Advanced Slime Paper MySQL Loader" - description = "MySQL loader for Advanced Slime Paper" + name = "Advanced Slime Paper SQL Loader" + description = "SQL loader for Advanced Slime Paper" } diff --git a/loaders/sql-loader/src/main/java/com/infernalsuite/asp/loaders/sql/SqlLoader.java b/loaders/sql-loader/src/main/java/com/infernalsuite/asp/loaders/sql/SqlLoader.java new file mode 100644 index 00000000..edae5ddf --- /dev/null +++ b/loaders/sql-loader/src/main/java/com/infernalsuite/asp/loaders/sql/SqlLoader.java @@ -0,0 +1,342 @@ +package com.infernalsuite.asp.loaders.sql; + +import com.infernalsuite.asp.api.exceptions.UnknownWorldException; +import com.infernalsuite.asp.api.loaders.UpdatableLoader; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.*; +import java.util.Locale; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class SqlLoader extends UpdatableLoader { + + private static final Logger LOGGER = LoggerFactory.getLogger(SqlLoader.class); + private static final Map DRIVERS = Map.of( + "h2", "org.h2.Driver" + ); + + private static final int CURRENT_DB_VERSION = 1; + + // Database version handling queries + private static final String CREATE_VERSIONING_TABLE_QUERY = "CREATE TABLE IF NOT EXISTS database_version (id INTEGER PRIMARY KEY, version INTEGER NOT NULL)"; + private static final String INSERT_VERSION_QUERY = "INSERT INTO database_version (id, version) VALUES (1, ?)"; + private static final String UPDATE_VERSION_QUERY = "UPDATE database_version SET version = ? WHERE id = 1"; + private static final String GET_VERSION_QUERY = "SELECT version FROM database_version WHERE id = 1"; + + // World handling queries + private static final String CREATE_WORLDS_TABLE_QUERY = "CREATE TABLE IF NOT EXISTS worlds (name VARCHAR(255) PRIMARY KEY, locked BIGINT NOT NULL DEFAULT 0, world BLOB NOT NULL)"; + private static final String SELECT_WORLD_QUERY = "SELECT world FROM worlds WHERE name = ?"; + private static final String DELETE_WORLD_QUERY = "DELETE FROM worlds WHERE name = ?"; + private static final String LIST_WORLDS_QUERY = "SELECT name FROM worlds"; + + private static final String MARIA_MYSQL_UPSERT_QUERY = "INSERT INTO worlds (name, world) VALUES (?, ?) ON DUPLICATE KEY UPDATE world = VALUES(world)"; + private static final String POSTGRESQL_SQLITE_UPSERT_QUERY = "INSERT INTO worlds (name, world) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET world = EXCLUDED.world"; + private static final String H2_UPSERT_QUERY = "MERGE INTO worlds (name, world) KEY(name) VALUES (?, ?)"; + + private static final String INSERT_WORLD_QUERY = "INSERT INTO worlds (name, world) VALUES (?, ?)"; + private static final String UPDATE_WORLD_QUERY = "UPDATE worlds SET world = ? WHERE name = ?"; + + private final HikariDataSource source; + + private String databaseType; + // Upsert query for fully supported databases + private String upsertQuery; + + public SqlLoader(String sqlURL, String host, int port, String database, boolean useSSL, String username, String password) throws SQLException { + HikariConfig hikariConfig = new HikariConfig(); + + sqlURL = sqlURL.replace("{host}", host); + sqlURL = sqlURL.replace("{port}", String.valueOf(port)); + sqlURL = sqlURL.replace("{database}", database); + sqlURL = sqlURL.replace("{usessl}", String.valueOf(useSSL)); + + hikariConfig.setJdbcUrl(sqlURL); + hikariConfig.setUsername(username); + hikariConfig.setPassword(password); + + String protocol = sqlURL.split(":")[1].toLowerCase(Locale.ROOT); + String driverClass = DRIVERS.get(protocol); + + if (driverClass != null) { + // We have to set a driver for some databases (e.g. h2) because Hikari can't detect it from the URL for some reason + hikariConfig.setDriverClassName(driverClass); + } + + if (protocol.equals("mysql") || protocol.equals("mariadb")) { + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + hikariConfig.addDataSourceProperty("useServerPrepStmts", "true"); + hikariConfig.addDataSourceProperty("useLocalSessionState", "true"); + hikariConfig.addDataSourceProperty("rewriteBatchedStatements", "true"); + hikariConfig.addDataSourceProperty("cacheResultSetMetadata", "true"); + hikariConfig.addDataSourceProperty("cacheServerConfiguration", "true"); + hikariConfig.addDataSourceProperty("elideSetAutoCommits", "true"); + hikariConfig.addDataSourceProperty("maintainTimeStats", "false"); + } + + source = new HikariDataSource(hikariConfig); + init(); + } + + public SqlLoader(HikariDataSource hikariDataSource) throws SQLException { + source = hikariDataSource; + init(); + } + + @Override + public void update() throws IOException, NewerStorageException { + try (Connection con = source.getConnection()) { + int version; + + try (PreparedStatement statement = con.prepareStatement(GET_VERSION_QUERY); + ResultSet set = statement.executeQuery()) { + version = set.next() ? set.getInt(1) : -1; + } + + if (version > CURRENT_DB_VERSION) { + throw new NewerStorageException(CURRENT_DB_VERSION, version); + } + + if (version < CURRENT_DB_VERSION && version != -1) { + LOGGER.warn("Your SWM database is outdated. The update process will start in 10 seconds."); + LOGGER.warn("Note that this update might make your database incompatible with older SWM versions."); + LOGGER.warn("Make sure no other servers with older SWM versions are using this database."); + LOGGER.warn("Shut down the server to prevent your database from being updated."); + + try { + Thread.sleep(10000L); + } catch (InterruptedException ignored) { + LOGGER.info("Update process aborted."); + return; + } + + // Update to v1: alter locked column to store a long + this.updateLockedColumn(con); + + try (PreparedStatement statement = con.prepareStatement(UPDATE_VERSION_QUERY)) { + statement.setInt(1, CURRENT_DB_VERSION); + statement.executeUpdate(); + } + } else if (version == -1) { + // Fresh database, just insert the version + try (PreparedStatement statement = con.prepareStatement(INSERT_VERSION_QUERY)) { + statement.setInt(1, CURRENT_DB_VERSION); + statement.executeUpdate(); + } + } + } catch (SQLException ex) { + throw new IOException(ex); + } + } + + @Override + public byte[] readWorld(String worldName) throws UnknownWorldException, IOException { + try (Connection con = source.getConnection(); + PreparedStatement statement = con.prepareStatement(SELECT_WORLD_QUERY)) { + statement.setString(1, worldName); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + throw new UnknownWorldException(worldName); + } + + return set.getBytes("world"); + } + } catch (SQLException ex) { + throw new IOException(ex); + } + } + + @Override + public boolean worldExists(String worldName) throws IOException { + try (Connection con = source.getConnection(); + PreparedStatement statement = con.prepareStatement(SELECT_WORLD_QUERY)) { + statement.setString(1, worldName); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException ex) { + throw new IOException(ex); + } + } + + @Override + public List listWorlds() throws IOException { + List worldList = new ArrayList<>(); + + try (Connection con = source.getConnection(); + PreparedStatement statement = con.prepareStatement(LIST_WORLDS_QUERY)) { + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + worldList.add(set.getString("name")); + } + } + } catch (SQLException ex) { + throw new IOException(ex); + } + + return worldList; + } + + @Override + public void saveWorld(String worldName, byte[] serializedWorld) throws IOException { + try (Connection con = source.getConnection()) { + if (upsertQuery != null) { + try (PreparedStatement statement = con.prepareStatement(upsertQuery)) { + statement.setString(1, worldName); + statement.setBytes(2, serializedWorld); + statement.executeUpdate(); + } + } else { + saveWorldsLegacy(con, worldName, serializedWorld); + } + } catch (SQLException ex) { + throw new IOException(ex); + } + } + + // Only for not fully supported databases. + private void saveWorldsLegacy(Connection connection, String worldName, byte[] serializedWorld) throws SQLException, IOException { + connection.setAutoCommit(false); + try { + int updated; + try (PreparedStatement statement = connection.prepareStatement(UPDATE_WORLD_QUERY)) { + statement.setBytes(1, serializedWorld); + statement.setString(2, worldName); + updated = statement.executeUpdate(); + } + + // Workaround upsert because there is no standard SQL syntax for it + if (updated == 0) { + try (PreparedStatement statement = connection.prepareStatement(INSERT_WORLD_QUERY)) { + statement.setString(1, worldName); + statement.setBytes(2, serializedWorld); + statement.executeUpdate(); + } + } + + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw new IOException(e); + } finally { + connection.setAutoCommit(true); + } + } + + @Override + public void saveWorlds(Map worlds) throws IOException { + if (worlds.isEmpty()) { + return; + } + + try (Connection con = source.getConnection()) { + if (upsertQuery != null) { + try (PreparedStatement statement = con.prepareStatement(upsertQuery)) { + for (Map.Entry entry : worlds.entrySet()) { + statement.setString(1, entry.getKey()); + statement.setBytes(2, entry.getValue()); + statement.addBatch(); + } + statement.executeBatch(); + } + } else { + saveWorldsLegacy(con, worlds); + } + } catch (SQLException ex) { + throw new IOException(ex); + } + } + + // Only for not fully supported databases. + private void saveWorldsLegacy(Connection con, Map worlds) throws SQLException { + con.setAutoCommit(false); + try { + try (PreparedStatement update = con.prepareStatement(UPDATE_WORLD_QUERY)) { + for (Map.Entry entry : worlds.entrySet()) { + update.setBytes(1, entry.getValue()); + update.setString(2, entry.getKey()); + update.addBatch(); + } + int[] results = update.executeBatch(); + + try (PreparedStatement insert = con.prepareStatement(INSERT_WORLD_QUERY)) { + int i = 0; + for (Map.Entry entry : worlds.entrySet()) { + if (results[i++] == 0) { + insert.setString(1, entry.getKey()); + insert.setBytes(2, entry.getValue()); + insert.addBatch(); + } + } + insert.executeBatch(); + } + } + con.commit(); + } catch (SQLException e) { + con.rollback(); + throw e; + } finally { + con.setAutoCommit(true); + } + } + + @Override + public void deleteWorld(String worldName) throws IOException, UnknownWorldException { + try (Connection con = source.getConnection(); + PreparedStatement statement = con.prepareStatement(DELETE_WORLD_QUERY)) { + statement.setString(1, worldName); + + if (statement.executeUpdate() == 0) { + throw new UnknownWorldException(worldName); + } + } catch (SQLException ex) { + throw new IOException(ex); + } + } + + public String getDatabaseType() { + return databaseType; + } + + private void init() throws SQLException { + try (Connection con = source.getConnection(); + Statement statement = con.createStatement()) { + this.databaseType = con.getMetaData().getDatabaseProductName().toLowerCase(Locale.ROOT); + + // Set upsert query for fully supported databases as it's faster than the insert + update + this.upsertQuery = switch (databaseType) { + case "mysql", "mariadb" -> MARIA_MYSQL_UPSERT_QUERY; + case "postgresql", "sqlite" -> POSTGRESQL_SQLITE_UPSERT_QUERY; + case "h2" -> H2_UPSERT_QUERY; + default -> null; + }; + + // Create worlds table + statement.execute(CREATE_WORLDS_TABLE_QUERY); + + // Create versioning table + statement.execute(CREATE_VERSIONING_TABLE_QUERY); + } + } + + private void updateLockedColumn(Connection con) throws SQLException { + // Migration is needed only from these databases + if (!databaseType.contains("mysql") && !databaseType.contains("mariadb")) { + return; + } + + try (Statement statement = con.createStatement()) { + statement.executeUpdate("ALTER TABLE worlds CHANGE COLUMN locked locked BIGINT NOT NULL DEFAULT 0"); + } + } +} diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 42da9d9e..1a8b3e46 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -28,9 +28,6 @@ tasks { relocate("org.bstats", "com.infernalsuite.asp.libs.bstats") relocate("org.spongepowered.configurate", "com.infernalsuite.asp.libs.configurate") - relocate("com.zaxxer.hikari", "com.infernalsuite.asp.libs.hikari") - relocate("com.mongodb", "com.infernalsuite.asp.libs.mongo") - relocate("io.lettuce", "com.infernalsuite.asp.libs.lettuce") relocate("org.bson", "com.infernalsuite.asp.libs.bson") } @@ -47,4 +44,5 @@ paper { main = "com.infernalsuite.asp.plugin.SWPlugin" authors = listOf("InfernalSuite") bootstrapper = "com.infernalsuite.asp.plugin.SlimePluginBootstrap" + loader = "com.infernalsuite.asp.plugin.SlimePluginLoader" } diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginBootstrap.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginBootstrap.java index 8c3ee7e4..d6a03a12 100644 --- a/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginBootstrap.java +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginBootstrap.java @@ -1,45 +1,43 @@ -package com.infernalsuite.asp.plugin; - -import io.papermc.paper.ServerBuildInfo; -import io.papermc.paper.plugin.bootstrap.BootstrapContext; -import io.papermc.paper.plugin.bootstrap.PluginBootstrap; -import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.logger.slf4j.ComponentLogger; - -import java.security.CodeSource; - -/* - * We use the boostrap to validate if the plugin is running on ASP. - * Doing this check in SWPlugin seems to not be as easy without significant changes - */ -public class SlimePluginBootstrap implements PluginBootstrap { - - @Override - public void bootstrap(BootstrapContext bootstrapContext) { - if(!ServerBuildInfo.buildInfo().isBrandCompatible(Key.key("infernalsuite", "advancedslimepaper"))) { - - ComponentLogger logger = bootstrapContext.getLogger(); - logger.error("================================================"); - logger.error("AdvancedSlimePaper Plugin - Incompatible Server"); - logger.error("================================================"); - logger.error(""); - logger.error("It looks like you're trying to run the AdvancedSlimePaper (ASP) plugin"); - logger.error("on a server that is NOT using the AdvancedSlimePaper software."); - logger.error(""); - logger.error("The ASP plugin only works with the AdvancedSlimePaper server jar."); - logger.error("Running it on PaperMC or other forks will not work."); - logger.error(""); - logger.error("To fix this, replace your current server jar"); - logger.error("with a AdvancedSlimePaper compatible jar from our website:"); - logger.error("https://infernalsuite.com"); - logger.error(""); - logger.error("If you need Purpur-specific features, we also maintain"); - logger.error("a less frequently updated Purpur fork that also provides Slime functionality."); - logger.error(""); - logger.error("================================================"); - - throw new UnsupportedOperationException("Attempted to run ASP plugin on non-ASP server"); - } - } - -} +package com.infernalsuite.asp.plugin; + +import io.papermc.paper.ServerBuildInfo; +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; + +/* + * We use the boostrap to validate if the plugin is running on ASP. + * Doing this check in SWPlugin seems to not be as easy without significant changes + */ +public class SlimePluginBootstrap implements PluginBootstrap { + + @Override + public void bootstrap(BootstrapContext bootstrapContext) { + if(!ServerBuildInfo.buildInfo().isBrandCompatible(Key.key("infernalsuite", "advancedslimepaper"))) { + + ComponentLogger logger = bootstrapContext.getLogger(); + logger.error("================================================"); + logger.error("AdvancedSlimePaper Plugin - Incompatible Server"); + logger.error("================================================"); + logger.error(""); + logger.error("It looks like you're trying to run the AdvancedSlimePaper (ASP) plugin"); + logger.error("on a server that is NOT using the AdvancedSlimePaper software."); + logger.error(""); + logger.error("The ASP plugin only works with the AdvancedSlimePaper server jar."); + logger.error("Running it on PaperMC or other forks will not work."); + logger.error(""); + logger.error("To fix this, replace your current server jar"); + logger.error("with a AdvancedSlimePaper compatible jar from our website:"); + logger.error("https://infernalsuite.com"); + logger.error(""); + logger.error("If you need Purpur-specific features, we also maintain"); + logger.error("a less frequently updated Purpur fork that also provides Slime functionality."); + logger.error(""); + logger.error("================================================"); + + throw new UnsupportedOperationException("Attempted to run ASP plugin on non-ASP server"); + } + } + +} diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginLoader.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginLoader.java new file mode 100644 index 00000000..0440f7b3 --- /dev/null +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/SlimePluginLoader.java @@ -0,0 +1,86 @@ +package com.infernalsuite.asp.plugin; + +import com.infernalsuite.asp.plugin.dependencies.DependenciesMavenResolver; +import com.infernalsuite.asp.plugin.dependencies.DependenciesVersions; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.loader.PluginClasspathBuilder; +import io.papermc.paper.plugin.loader.PluginLoader; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Map; + +public class SlimePluginLoader implements PluginLoader { + + private static final Path CONFIG_PATH = Path.of("plugins", "SlimeWorldManager", "sources.yml"); + + @Override + public void classloader(PluginClasspathBuilder builder) { + File configFile = CONFIG_PATH.toFile(); + + if (!configFile.exists()) { + return; + } + + DependenciesMavenResolver resolver = new DependenciesMavenResolver(); + resolver.addRepositories(); + + try (InputStream is = new FileInputStream(configFile)) { + Map config = new Yaml().load(is); + + loadSqlDependencies(builder.getContext(), config, resolver); + loadMongoDependencies(config, resolver); + loadRedisDependencies(config, resolver); + + builder.addLibrary(resolver.getResolver()); + } catch (Exception exception) { + exception.printStackTrace(); + } + } + + private void loadSqlDependencies(PluginProviderContext context, Map config, DependenciesMavenResolver resolver) { + if (!isEnabled(config, "sql")) { + return; + } + + resolver.addDependency("com.zaxxer", "HikariCP", DependenciesVersions.HIKARI); + String url = getNestedString(config, "sql", "sqlUrl"); + if (url == null || !url.startsWith("jdbc:")) { + return; + } + + String driverType = url.split(":")[1].toLowerCase(); + + switch (driverType) { + case "mysql" -> resolver.addDependency("com.mysql", "mysql-connector-j", DependenciesVersions.MYSQL); + case "mariadb" -> resolver.addDependency("org.mariadb.jdbc", "mariadb-java-client", DependenciesVersions.MARIADB); + case "postgresql" -> resolver.addDependency("org.postgresql", "postgresql", DependenciesVersions.POSTGRES); + case "sqlite" -> resolver.addDependency("org.xerial", "sqlite-jdbc", DependenciesVersions.SQLITE); + case "h2" -> resolver.addDependency("com.h2database", "h2", DependenciesVersions.H2); + default -> context.getLogger().warn("Unsupported SQL driver type '{}', no SQL dependencies will be loaded.", driverType); + } + } + + private void loadMongoDependencies(Map config, DependenciesMavenResolver resolver) { + if (isEnabled(config, "mongodb")) { + resolver.addDependency("org.mongodb", "mongodb-driver-sync", DependenciesVersions.MONGODB); + } + } + + private void loadRedisDependencies(Map config, DependenciesMavenResolver resolver) { + if (isEnabled(config, "redis")) { + resolver.addDependency("io.lettuce", "lettuce-core", DependenciesVersions.LETTUCE); + } + } + + private boolean isEnabled(Map config, String key) { + return config.get(key) instanceof Map section && Boolean.TRUE.equals(section.get("enabled")); + } + + private String getNestedString(Map config, String sectionKey, String valueKey) { + return config.get(sectionKey) instanceof Map section ? String.valueOf(section.get(valueKey)) : null; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/config/ConfigManager.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/config/ConfigManager.java index 6dba89ba..df5e16e0 100644 --- a/plugin/src/main/java/com/infernalsuite/asp/plugin/config/ConfigManager.java +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/config/ConfigManager.java @@ -2,6 +2,8 @@ import com.infernalsuite.asp.plugin.SWPlugin; import io.leangen.geantyref.TypeToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.spongepowered.configurate.loader.HeaderMode; import org.spongepowered.configurate.yaml.NodeStyle; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; @@ -32,6 +34,8 @@ public static void initialize() throws IOException { .nodeStyle(NodeStyle.BLOCK).headerMode(HeaderMode.PRESERVE).build(); datasourcesConfig = datasourcesConfigLoader.load().get(TypeToken.get(DatasourcesConfig.class)); + migrateSources(); + worldConfig.save(); datasourcesConfigLoader.save(datasourcesConfigLoader.createNode().set(TypeToken.get(DatasourcesConfig.class), datasourcesConfig)); } @@ -48,6 +52,46 @@ private static void copyDefaultConfigs() throws IOException { } } + // Migrate mysql to sql format + private static void migrateSources() throws IOException { + if (!SOURCES_FILE.exists()) { + return; + } + + YamlConfigurationLoader loader = YamlConfigurationLoader.builder().file(SOURCES_FILE) + .nodeStyle(NodeStyle.BLOCK).headerMode(HeaderMode.PRESERVE).build(); + var root = loader.load(); + + if (!root.node("mysql").virtual()) { + Logger logger = LoggerFactory.getLogger(ConfigManager.class); + logger.info("Starting configuration migration for datasources..."); + + var mysqlNode = root.node("mysql"); + DatasourcesConfig.SqlConfig sql = new DatasourcesConfig.SqlConfig(); + sql.setHost(mysqlNode.node("host").getString("127.0.0.1")); + sql.setPort(mysqlNode.node("port").getInt(3306)); + sql.setUsername(mysqlNode.node("username").getString("slimeworldmanager")); + sql.setPassword(mysqlNode.node("password").getString("")); + sql.setDatabase(mysqlNode.node("database").getString("slimeworldmanager")); + sql.setUsessl(mysqlNode.node("usessl").getBoolean(false)); + + String defaultUrl = String.format("jdbc:mysql://%s:%d/%s?autoReconnect=true&allowMultiQueries=true&useSSL=%s", + sql.getHost(), sql.getPort(), sql.getDatabase(), sql.isUsessl()); + sql.setSqlUrl(mysqlNode.node("sqlUrl").getString(defaultUrl)); + + if (datasourcesConfig == null) { + datasourcesConfig = new DatasourcesConfig(); + } + + datasourcesConfig.setSqlConfig(sql); + + root.node("mysql").raw(null); + loader.save(root); + + logger.info("Migrated configuration datasources"); + } + } + public static DatasourcesConfig getDatasourcesConfig() { return datasourcesConfig; } diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/config/DatasourcesConfig.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/config/DatasourcesConfig.java index 1b8e0677..b3a80947 100644 --- a/plugin/src/main/java/com/infernalsuite/asp/plugin/config/DatasourcesConfig.java +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/config/DatasourcesConfig.java @@ -8,8 +8,8 @@ public class DatasourcesConfig { @Setting("file") private FileConfig fileConfig = new FileConfig(); - @Setting("mysql") - private MysqlConfig mysqlConfig = new MysqlConfig(); + @Setting("sql") + private SqlConfig sqlConfig = new SqlConfig(); @Setting("mongodb") private MongoDBConfig mongoDbConfig = new MongoDBConfig(); @Setting("redis") @@ -19,7 +19,7 @@ public class DatasourcesConfig { private APIConfig apiConfig = new APIConfig(); @ConfigSerializable - public static class MysqlConfig { + public static class SqlConfig { @Setting("enabled") private boolean enabled = false; @@ -280,12 +280,12 @@ public void setFileConfig(FileConfig fileConfig) { this.fileConfig = fileConfig; } - public MysqlConfig getMysqlConfig() { - return mysqlConfig; + public SqlConfig getSqlConfig() { + return sqlConfig; } - public void setMysqlConfig(MysqlConfig mysqlConfig) { - this.mysqlConfig = mysqlConfig; + public void setSqlConfig(SqlConfig sqlConfig) { + this.sqlConfig = sqlConfig; } public MongoDBConfig getMongoDbConfig() { diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/config/WorldData.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/config/WorldData.java index 5a84322d..ca515ee2 100644 --- a/plugin/src/main/java/com/infernalsuite/asp/plugin/config/WorldData.java +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/config/WorldData.java @@ -1,6 +1,5 @@ package com.infernalsuite.asp.plugin.config; -import com.infernalsuite.asp.api.world.properties.SlimeProperties; import com.infernalsuite.asp.api.world.properties.SlimePropertyMap; import org.bukkit.Difficulty; import org.bukkit.World; diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/dependencies/DependenciesMavenResolver.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/dependencies/DependenciesMavenResolver.java new file mode 100644 index 00000000..f4ff21fb --- /dev/null +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/dependencies/DependenciesMavenResolver.java @@ -0,0 +1,35 @@ +package com.infernalsuite.asp.plugin.dependencies; + +import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.repository.RemoteRepository; + +public class DependenciesMavenResolver { + + private final MavenLibraryResolver resolver = new MavenLibraryResolver(); + + public void addRepositories() { + this.resolver.addRepository(new RemoteRepository.Builder( + "central", + "default", + MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR + ).build()); + + this.resolver.addRepository(new RemoteRepository.Builder( + "is-releases", + "default", + "https://repo.infernalsuite.com/repository/maven-releases/" + ).build()); + } + + public void addDependency(String groupId, String artifactId, String version) { + Artifact artifact = new DefaultArtifact(groupId, artifactId, "jar", version); + this.resolver.addDependency(new Dependency(artifact, null)); + } + + public MavenLibraryResolver getResolver() { + return resolver; + } +} diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/dependencies/DependenciesVersions.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/dependencies/DependenciesVersions.java new file mode 100644 index 00000000..56e66d96 --- /dev/null +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/dependencies/DependenciesVersions.java @@ -0,0 +1,14 @@ +package com.infernalsuite.asp.plugin.dependencies; + +public class DependenciesVersions { + + public static final String HIKARI = "6.2.1"; + public static final String MONGODB = "5.2.1"; + public static final String LETTUCE = "6.5.1.RELEASE"; + public static final String MYSQL = "8.0.33"; + public static final String MARIADB = "3.5.7"; + public static final String POSTGRES = "42.7.9"; + public static final String SQLITE = "3.51.1.0"; + public static final String H2 = "2.4.240"; + +} diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/loader/LoaderManager.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/loader/LoaderManager.java index ccf09ad7..4096c900 100644 --- a/plugin/src/main/java/com/infernalsuite/asp/plugin/loader/LoaderManager.java +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/loader/LoaderManager.java @@ -5,16 +5,15 @@ import com.infernalsuite.asp.loaders.api.APILoader; import com.infernalsuite.asp.loaders.file.FileLoader; import com.infernalsuite.asp.loaders.mongo.MongoLoader; -import com.infernalsuite.asp.loaders.mysql.MysqlLoader; +import com.infernalsuite.asp.loaders.sql.SqlLoader; import com.infernalsuite.asp.loaders.redis.RedisLoader; -import com.mongodb.MongoException; -import io.lettuce.core.RedisException; +import com.infernalsuite.asp.plugin.config.DatasourcesConfig; +import com.infernalsuite.asp.plugin.util.ThrowingSupplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.sql.SQLException; import java.util.HashMap; import java.util.Map; @@ -31,59 +30,66 @@ public LoaderManager() { com.infernalsuite.asp.plugin.config.DatasourcesConfig.FileConfig fileConfig = config.getFileConfig(); registerLoader("file", new FileLoader(new File(fileConfig.getPath()))); - // Mysql loader - com.infernalsuite.asp.plugin.config.DatasourcesConfig.MysqlConfig mysqlConfig = config.getMysqlConfig(); - if (mysqlConfig.isEnabled()) { + // Sql loader + DatasourcesConfig.SqlConfig sqlConfig = config.getSqlConfig(); + if (sqlConfig.isEnabled()) { + ThrowingSupplier sqlSupplier = () -> new SqlLoader( + sqlConfig.getSqlUrl(), + sqlConfig.getHost(), sqlConfig.getPort(), + sqlConfig.getDatabase(), sqlConfig.isUsessl(), + sqlConfig.getUsername(), sqlConfig.getPassword() + ); + try { - registerLoader("mysql", new MysqlLoader( - mysqlConfig.getSqlUrl(), - mysqlConfig.getHost(), mysqlConfig.getPort(), - mysqlConfig.getDatabase(), mysqlConfig.isUsessl(), - mysqlConfig.getUsername(), mysqlConfig.getPassword() - )); - } catch (final SQLException ex) { - LOGGER.error("Failed to establish connection to the MySQL server:", ex); + SqlLoader sqlLoader = sqlSupplier.get(); + tryRegisterLoader(sqlLoader.getDatabaseType(), () -> sqlLoader); + } catch (Exception e) { + LOGGER.error("Failed to establish connection to the loader:", e); } } // MongoDB loader com.infernalsuite.asp.plugin.config.DatasourcesConfig.MongoDBConfig mongoConfig = config.getMongoDbConfig(); - if (mongoConfig.isEnabled()) { - try { - registerLoader("mongodb", new MongoLoader( - mongoConfig.getDatabase(), - mongoConfig.getCollection(), - mongoConfig.getUsername(), - mongoConfig.getPassword(), - mongoConfig.getAuthSource(), - mongoConfig.getHost(), - mongoConfig.getPort(), - mongoConfig.getUri() - )); - } catch (final MongoException ex) { - LOGGER.error("Failed to establish connection to the MongoDB server:", ex); - } + tryRegisterLoader("mongo", () -> new MongoLoader( + mongoConfig.getDatabase(), + mongoConfig.getCollection(), + mongoConfig.getUsername(), + mongoConfig.getPassword(), + mongoConfig.getAuthSource(), + mongoConfig.getHost(), + mongoConfig.getPort(), + mongoConfig.getUri() + )); } com.infernalsuite.asp.plugin.config.DatasourcesConfig.RedisConfig redisConfig = config.getRedisConfig(); - if (redisConfig.isEnabled()){ - try { - registerLoader("redis", new RedisLoader(redisConfig.getUri())); - } catch (final RedisException ex) { - LOGGER.error("Failed to establish connection to the Redis server:", ex); - } + if (redisConfig.isEnabled()) { + tryRegisterLoader("redis", () -> new RedisLoader(redisConfig.getUri())); } com.infernalsuite.asp.plugin.config.DatasourcesConfig.APIConfig apiConfig = config.getApiConfig(); - if(apiConfig.isEnabled()){ - registerLoader("api", new APILoader( + if(apiConfig.isEnabled()) { + tryRegisterLoader("api", () -> new APILoader( apiConfig.getUrl(), apiConfig.getUsername(), apiConfig.getToken(), apiConfig.isIgnoreSslCertificate() )); } + + if (getLoaders().isEmpty()) { + throw new IllegalStateException("No valid data source configuration found! Please check your config file."); + } + } + + private void tryRegisterLoader(String name, ThrowingSupplier supplier) { + try { + SlimeLoader loader = supplier.get(); + registerLoader(name, loader); + } catch (Exception ex) { + LOGGER.error("Failed to establish connection to the loader:", ex); + } } public void registerLoader(String dataSource, SlimeLoader loader) { diff --git a/plugin/src/main/java/com/infernalsuite/asp/plugin/util/ThrowingSupplier.java b/plugin/src/main/java/com/infernalsuite/asp/plugin/util/ThrowingSupplier.java new file mode 100644 index 00000000..20ee1daa --- /dev/null +++ b/plugin/src/main/java/com/infernalsuite/asp/plugin/util/ThrowingSupplier.java @@ -0,0 +1,7 @@ +package com.infernalsuite.asp.plugin.util; + +public interface ThrowingSupplier { + + T get() throws Exception; + +} diff --git a/plugin/src/main/resources/sources.yml b/plugin/src/main/resources/sources.yml index c15b5951..a79bf55f 100644 --- a/plugin/src/main/resources/sources.yml +++ b/plugin/src/main/resources/sources.yml @@ -1,6 +1,6 @@ # Inside this file is the configuration options # for the data sources that ASP supports -mysql: +sql: enabled: false host: 127.0.0.1 port: 3306 @@ -8,6 +8,12 @@ mysql: password: '' database: slimeworldmanager usessl: false + # MySQL: jdbc:mysql://{host}:{port}/{database}?autoReconnect=true&allowMultiQueries=true&useSSL={usessl} + # MariaDB: jdbc:mariadb://{host}:{port}/{database}?autoReconnect=true&allowMultiQueries=true&useSSL={usessl} + # PostgreSQL: jdbc:postgresql://{host}:{port}/{database}?ssl={usessl} + # SQLite: jdbc:sqlite:filename.db + # H2: jdbc:h2:./data/filename + sqlUrl: 'jdbc:mysql://{host}:{port}/{database}?autoReconnect=true&allowMultiQueries=true&useSSL={usessl}' mongodb: enabled: false host: 127.0.0.1 diff --git a/settings.gradle.kts b/settings.gradle.kts index d38dbeaf..38c4af21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,7 +27,7 @@ include("loaders:api-loader") findProject(":loaders:api-loader")?.name = "api-loader" include("loaders:file-loader") findProject(":loaders:file-loader")?.name = "file-loader" -include("loaders:mysql-loader") -findProject(":loaders:mysql-loader")?.name = "mysql-loader" +include("loaders:sql-loader") +findProject(":loaders:sql-loader")?.name = "sql-loader" include("loaders:redis-loader") findProject(":loaders:redis-loader")?.name = "redis-loader"