From 7f6b2a8ce2cc992e8908a569e8a9977730bb2708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Krzycha=C5=82a?= Date: Sat, 13 Dec 2025 15:00:47 +0100 Subject: [PATCH] Add PostgresqlLoader Took 57 minutes --- loaders/build.gradle.kts | 1 + loaders/postgresql-loader/build.gradle | 16 ++ .../loaders/postgresql/PostgresqlLoader.java | 201 ++++++++++++++++++ plugin/build.gradle.kts | 1 + .../asp/plugin/config/DatasourcesConfig.java | 101 +++++++++ .../asp/plugin/loader/LoaderManager.java | 16 ++ plugin/src/main/resources/sources.yml | 8 + settings.gradle.kts | 4 +- 8 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 loaders/postgresql-loader/build.gradle create mode 100644 loaders/postgresql-loader/src/main/java/com/infernalsuite/asp/loaders/postgresql/PostgresqlLoader.java diff --git a/loaders/build.gradle.kts b/loaders/build.gradle.kts index 73dc4375..31e12d2b 100644 --- a/loaders/build.gradle.kts +++ b/loaders/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { api(project(":loaders:mongo-loader")) api(project(":loaders:mysql-loader")) api(project(":loaders:redis-loader")) + api(project(":loaders:postgresql-loader")) compileOnly(paperApi()) } diff --git a/loaders/postgresql-loader/build.gradle b/loaders/postgresql-loader/build.gradle new file mode 100644 index 00000000..20bbe622 --- /dev/null +++ b/loaders/postgresql-loader/build.gradle @@ -0,0 +1,16 @@ +plugins { + id("asp.base-conventions") + id("asp.publishing-conventions") +} + +dependencies { + compileOnly(project(":api")) + + api(libs.hikari) + api("org.postgresql:postgresql:42.7.8") +} + +publishConfiguration { + name = "Advanced Slime Paper PostgresSQL Loader" + description = "PostgresSQL loader for Advanced Slime Paper" +} diff --git a/loaders/postgresql-loader/src/main/java/com/infernalsuite/asp/loaders/postgresql/PostgresqlLoader.java b/loaders/postgresql-loader/src/main/java/com/infernalsuite/asp/loaders/postgresql/PostgresqlLoader.java new file mode 100644 index 00000000..98a06b0c --- /dev/null +++ b/loaders/postgresql-loader/src/main/java/com/infernalsuite/asp/loaders/postgresql/PostgresqlLoader.java @@ -0,0 +1,201 @@ +package com.infernalsuite.asp.loaders.postgresql; + + +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 PostgresqlLoader extends UpdatableLoader { + + private static final Logger LOGGER = LoggerFactory.getLogger(PostgresqlLoader.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 SERIAL PRIMARY KEY, " + + "version INT" + + ");"; + + private static final String INSERT_VERSION_QUERY = + "INSERT INTO database_version (id, version) " + + "VALUES (1, ?) " + + "ON CONFLICT (id) DO UPDATE SET version = EXCLUDED.version;"; + + 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 (" + + "id SERIAL PRIMARY KEY, " + + "name VARCHAR(255) UNIQUE, " + + "locked BIGINT DEFAULT 0 NOT NULL, " + + "world BYTEA" + + ");"; + + 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 CONFLICT (name) DO UPDATE SET world = EXCLUDED.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 PostgresqlLoader(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) + .replace("{port}", String.valueOf(port)) + .replace("{database}", database) + .replace("{usessl}", String.valueOf(useSSL)); + + hikariConfig.setJdbcUrl(sqlURL); + hikariConfig.setUsername(username); + hikariConfig.setPassword(password); + hikariConfig.setDriverClassName("org.postgresql.Driver"); + + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + source = new HikariDataSource(hikariConfig); + init(); + } + + @ApiStatus.Experimental + public PostgresqlLoader(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 PostgreSQL database is outdated. Updating now..."); + try { + Thread.sleep(5000L); + } catch (InterruptedException ignored) { + } + + // Insert/update database 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); + 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.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()) { + try (PreparedStatement statement = con.prepareStatement(CREATE_WORLDS_TABLE_QUERY)) { + statement.execute(); + } + try (PreparedStatement statement = con.prepareStatement(CREATE_VERSIONING_TABLE_QUERY)) { + statement.execute(); + } + } + } +} diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 42da9d9e..95ac4074 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -31,6 +31,7 @@ tasks { 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.postgresql", "com.infernalsuite.asp.libs.postgresql") relocate("org.bson", "com.infernalsuite.asp.libs.bson") } 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..4aceaf90 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 @@ -10,6 +10,8 @@ public class DatasourcesConfig { private FileConfig fileConfig = new FileConfig(); @Setting("mysql") private MysqlConfig mysqlConfig = new MysqlConfig(); + @Setting("postgresql") + private PostgresqlConfig postgresqlConfig = new PostgresqlConfig(); @Setting("mongodb") private MongoDBConfig mongoDbConfig = new MongoDBConfig(); @Setting("redis") @@ -108,6 +110,97 @@ public void setSqlUrl(String sqlUrl) { } } + @ConfigSerializable + public static class PostgresqlConfig { + + @Setting("enabled") + private boolean enabled = false; + + @Setting("host") + private String host = "127.0.0.1"; + @Setting("port") + private int port = 5432; + + @Setting("username") + private String username = "slimeworldmanager"; + @Setting("password") + private String password = ""; + + @Setting("database") + private String database = "slimeworldmanager"; + + @Setting("usessl") + private boolean usessl = false; + + @Setting("sqlUrl") + private String sqlUrl = "jdbc:postgresql://{host}:{port}/{database}?ssl={usessl}";; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public boolean isUsessl() { + return usessl; + } + + public void setUsessl(boolean usessl) { + this.usessl = usessl; + } + + public String getSqlUrl() { + return sqlUrl; + } + + public void setSqlUrl(String sqlUrl) { + this.sqlUrl = sqlUrl; + } + + } + @ConfigSerializable public static class MongoDBConfig { @@ -288,6 +381,14 @@ public void setMysqlConfig(MysqlConfig mysqlConfig) { this.mysqlConfig = mysqlConfig; } + public PostgresqlConfig getPostgresqlConfig() { + return postgresqlConfig; + } + + public void setPostgresqlConfig(PostgresqlConfig postgresqlConfig) { + this.postgresqlConfig = postgresqlConfig; + } + public MongoDBConfig getMongoDbConfig() { return mongoDbConfig; } 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..14be624b 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 @@ -6,6 +6,7 @@ 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.postgresql.PostgresqlLoader; import com.infernalsuite.asp.loaders.redis.RedisLoader; import com.mongodb.MongoException; import io.lettuce.core.RedisException; @@ -46,6 +47,21 @@ public LoaderManager() { } } + // Postgresql loader + com.infernalsuite.asp.plugin.config.DatasourcesConfig.PostgresqlConfig postgresqlConfig = config.getPostgresqlConfig(); + if (postgresqlConfig.isEnabled()) { + try { + registerLoader("postgresql", new PostgresqlLoader( + postgresqlConfig.getSqlUrl(), + postgresqlConfig.getHost(), postgresqlConfig.getPort(), + postgresqlConfig.getDatabase(), postgresqlConfig.isUsessl(), + postgresqlConfig.getUsername(), postgresqlConfig.getPassword() + )); + } catch (final SQLException ex) { + LOGGER.error("Failed to establish connection to the PostgresSQL server:", ex); + } + } + // MongoDB loader com.infernalsuite.asp.plugin.config.DatasourcesConfig.MongoDBConfig mongoConfig = config.getMongoDbConfig(); diff --git a/plugin/src/main/resources/sources.yml b/plugin/src/main/resources/sources.yml index c15b5951..a17a6fe3 100644 --- a/plugin/src/main/resources/sources.yml +++ b/plugin/src/main/resources/sources.yml @@ -8,6 +8,14 @@ mysql: password: '' database: slimeworldmanager usessl: false +postgresql: + enabled: false + host: 127.0.0.1 + port: 5432 + username: slimeworldmanager + password: '' + database: slimeworldmanager + usessl: false mongodb: enabled: false host: 127.0.0.1 diff --git a/settings.gradle.kts b/settings.gradle.kts index d38dbeaf..2f450752 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,5 +29,7 @@ include("loaders:file-loader") findProject(":loaders:file-loader")?.name = "file-loader" include("loaders:mysql-loader") findProject(":loaders:mysql-loader")?.name = "mysql-loader" +include("loaders:postgresql-loader") +findProject(":loaders:postgresql-loader")?.name = "postgresql-loader" include("loaders:redis-loader") -findProject(":loaders:redis-loader")?.name = "redis-loader" +findProject(":loaders:redis-loader")?.name = "redis-loader" \ No newline at end of file