diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java index f3c9de6..80ff023 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java @@ -6,7 +6,7 @@ import org.bstats.charts.SimplePie; import xyz.earthcow.networkjoinmessages.common.ConfigManager; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; -import xyz.earthcow.networkjoinmessages.common.storage.SQLPlayerJoinTracker; +import xyz.earthcow.networkjoinmessages.common.storage.SQLConfig; import java.util.*; import java.util.concurrent.ThreadLocalRandom; @@ -308,11 +308,11 @@ private String selectMessage(String definite, List pool) { // --- SQL config builder --- /** - * Builds a {@link SQLPlayerJoinTracker.SQLConfig} from the values loaded during {@link #reload()}. + * Builds a {@link SQLConfig} from the values loaded during {@link #reload()}. * Only meaningful when {@link #getStorageType()} is {@code "SQL"}. */ - public SQLPlayerJoinTracker.SQLConfig buildSqlConfig() { - return new SQLPlayerJoinTracker.SQLConfig( + public SQLConfig buildSqlConfig() { + return new SQLConfig( sqlHost, sqlPort, sqlDatabase, sqlUsername, sqlPassword, sqlDriver, sqlTablePrefix, sqlUseSSL, sqlConnectionTimeout diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLConfig.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLConfig.java new file mode 100644 index 0000000..a8af97d --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLConfig.java @@ -0,0 +1,16 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +/** + * Immutable value object carrying the SQL connection parameters read from config. + */ +public record SQLConfig( + String host, + int port, + String database, + String username, + String password, + String driver, + String tablePrefix, + boolean useSSL, + int connectionTimeout +) {} diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java new file mode 100644 index 0000000..e10586d --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java @@ -0,0 +1,88 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; +import xyz.earthcow.networkjoinmessages.common.util.SQLDriverLoader; + +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +abstract class SQLHandler implements AutoCloseable { + protected final CoreLogger logger; + protected final SQLConfig sqlConfig; + protected final boolean isPostgres; + private final String logPrefix; + private Connection connection; + + protected SQLHandler(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder) + throws SQLException, SQLDriverLoader.DriverLoadException { + this.logger = logger; + this.sqlConfig = sqlConfig; + this.isPostgres = "postgresql".equals(sqlConfig.driver()); + this.logPrefix = "[" + getClass().getSimpleName() + "] "; + new SQLDriverLoader(logger, dataFolder).ensureLoaded(sqlConfig.driver()); + setUpConnection(); + } + + protected abstract String createTableSql(); + + // Provided to subclasses + protected synchronized Connection connection() { + return this.connection; + } + + /** + * Returns {@code true} if the connection is unusable, attempting a reconnect first. + */ + protected synchronized boolean isConnectionInvalid() { + try { + if (connection == null || connection.isClosed() || !connection.isValid(sqlConfig.connectionTimeout())) { + logger.info(logPrefix + "Connection lost — attempting reconnect..."); + setUpConnection(); + } + return false; + } catch (SQLException e) { + logger.severe(logPrefix + "Cannot reach SQL server at '" + sqlConfig.host() + "': " + e.getMessage()); + return true; + } + } + + /** + * Opens a new connection and ensures the table exists. + */ + private void setUpConnection() throws SQLException { + String url = buildJdbcUrl(); + this.connection = DriverManager.getConnection(url, sqlConfig.username(), sqlConfig.password()); + try (Statement stmt = connection.createStatement()) { + stmt.execute(createTableSql()); + } + logger.debug(logPrefix + "Connected to " + sqlConfig.driver() + " at " + sqlConfig.host() + ":" + sqlConfig.port()); + } + + private String buildJdbcUrl() { + StringBuilder url = new StringBuilder() + .append("jdbc:").append(sqlConfig.driver()).append("://") + .append(sqlConfig.host()).append(':').append(sqlConfig.port()) + .append('/').append(sqlConfig.database()) + .append("?autoReconnect=true") + .append("&connectTimeout=").append(sqlConfig.connectionTimeout() * 1000) + .append("&allowPublicKeyRetrieval=true"); + + if (!isPostgres) { + url.append("&useSSL=").append(sqlConfig.useSSL()); + url.append("&characterEncoding=utf8"); + } + + return url.toString(); + } + + @Override + public synchronized void close() throws SQLException { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + +} diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java index b740ff0..74ce6cc 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java @@ -14,15 +14,15 @@ * *

Supports MySQL, MariaDB, and PostgreSQL. The required JDBC driver JAR is * downloaded automatically from Maven Central on first use and cached in - * {@code /drivers/}. See {@link SQLDriverLoader}. + * {@code /drivers/}. See {@link SQLDriverLoader}. * - *

Connection details are supplied via {@link SQLPlayerJoinTracker.SQLConfig}. + *

Connection details are supplied via {@link SQLConfig}. * The store keeps a single persistent {@link Connection} and transparently * reconnects on failure. * *

All public methods are {@code synchronized} for thread safety. */ -public class SQLPlayerDataStore implements PlayerDataStore { +public class SQLPlayerDataStore extends SQLHandler implements PlayerDataStore { // MySQL / MariaDB uses INSERT … ON DUPLICATE KEY UPDATE. // PostgreSQL uses INSERT … ON CONFLICT DO UPDATE. @@ -33,17 +33,9 @@ public class SQLPlayerDataStore implements PlayerDataStore { private final String UPSERT_MYSQL; private final String UPSERT_POSTGRES; - private final CoreLogger logger; - private final SQLPlayerJoinTracker.SQLConfig sqlConfig; - private final boolean isPostgres; - private Connection connection; - - public SQLPlayerDataStore(CoreLogger logger, SQLPlayerJoinTracker.SQLConfig sqlConfig, Path pluginDataFolder) + public SQLPlayerDataStore(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder) throws SQLException, SQLDriverLoader.DriverLoadException { - this.logger = logger; - this.sqlConfig = sqlConfig; - this.isPostgres = "postgresql".equals(sqlConfig.driver()); - new SQLDriverLoader(logger, pluginDataFolder).ensureLoaded(sqlConfig.driver()); + super(logger, sqlConfig, dataFolder); String tableName = sqlConfig.tablePrefix() + "players"; @@ -92,14 +84,18 @@ public SQLPlayerDataStore(CoreLogger logger, SQLPlayerJoinTracker.SQLConfig sqlC " ignore_swap = EXCLUDED.ignore_swap," + " ignore_leave = EXCLUDED.ignore_leave"; - setUpConnection(); + } + + @Override + protected String createTableSql() { + return isPostgres ? CREATE_TABLE_POSTGRES : CREATE_TABLE_MYSQL; } @Override @Nullable public synchronized PlayerDataSnapshot getData(UUID playerUuid) { if (isConnectionInvalid()) return null; - try (PreparedStatement ps = connection.prepareStatement(SELECT_SQL)) { + try (PreparedStatement ps = connection().prepareStatement(SELECT_SQL)) { ps.setString(1, playerUuid.toString()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { @@ -122,7 +118,7 @@ public synchronized PlayerDataSnapshot getData(UUID playerUuid) { public synchronized void saveData(UUID playerUuid, PlayerDataSnapshot data) { if (isConnectionInvalid()) return; String upsert = isPostgres ? UPSERT_POSTGRES : UPSERT_MYSQL; - try (PreparedStatement ps = connection.prepareStatement(upsert)) { + try (PreparedStatement ps = connection().prepareStatement(upsert)) { ps.setString(1, playerUuid.toString()); ps.setString(2, data.playerName()); ps.setObject(3, data.silentState(), Types.BOOLEAN); @@ -139,7 +135,7 @@ public synchronized void saveData(UUID playerUuid, PlayerDataSnapshot data) { @Nullable public synchronized UUID resolveUuid(String playerName) { if (isConnectionInvalid()) return null; - try (PreparedStatement ps = connection.prepareStatement(RESOLVE_SQL)) { + try (PreparedStatement ps = connection().prepareStatement(RESOLVE_SQL)) { ps.setString(1, playerName); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { @@ -151,67 +147,4 @@ public synchronized UUID resolveUuid(String playerName) { } return null; } - - @Override - public synchronized void close() throws SQLException { - if (connection != null && !connection.isClosed()) { - connection.close(); - } - } - - // --- Internal helpers --- - - /** - * Opens a new connection and ensures the table exists. - */ - private void setUpConnection() throws SQLException { - String url = buildJdbcUrl(); - this.connection = DriverManager.getConnection(url, sqlConfig.username(), sqlConfig.password()); - try (Statement stmt = connection.createStatement()) { - stmt.execute(isPostgres ? CREATE_TABLE_POSTGRES : CREATE_TABLE_MYSQL); - } - logger.debug("[SQLPlayerDataStore] Connected to " + sqlConfig.driver() + " at " + sqlConfig.host() + ":" + sqlConfig.port()); - } - - /** - * Returns {@code true} if the connection is unusable, attempting a reconnect first. - */ - private boolean isConnectionInvalid() { - try { - if (connection == null || connection.isClosed() || !connection.isValid(sqlConfig.connectionTimeout())) { - logger.info("[SQLPlayerDataStore] Connection lost — attempting reconnect..."); - setUpConnection(); - } - return false; - } catch (SQLException e) { - logger.severe("[SQLPlayerDataStore] Cannot reach SQL server at '" + sqlConfig.host() + "': " + e.getMessage()); - return true; - } - } - - /** - * Builds a JDBC URL from the {@link SQLPlayerJoinTracker.SQLConfig}. - * - *

- */ - private String buildJdbcUrl() { - StringBuilder url = new StringBuilder() - .append("jdbc:").append(sqlConfig.driver()).append("://") - .append(sqlConfig.host()).append(':').append(sqlConfig.port()) - .append('/').append(sqlConfig.database()) - .append("?autoReconnect=true") - .append("&connectTimeout=").append(sqlConfig.connectionTimeout() * 1000) - .append("&allowPublicKeyRetrieval=true"); - - if (!isPostgres) { - url.append("&useSSL=").append(sqlConfig.useSSL()); - url.append("&characterEncoding=utf8"); - } - - return url.toString(); - } } \ No newline at end of file diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java index 324098d..fec9be3 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java @@ -19,7 +19,7 @@ * *

All public methods are {@code synchronized} for thread safety. */ -public class SQLPlayerJoinTracker implements PlayerJoinTracker { +public class SQLPlayerJoinTracker extends SQLHandler implements PlayerJoinTracker { // MySQL / MariaDB uses INSERT … ON DUPLICATE KEY UPDATE. // PostgreSQL uses INSERT … ON CONFLICT DO NOTHING. @@ -29,32 +29,9 @@ public class SQLPlayerJoinTracker implements PlayerJoinTracker { private final String UPSERT_MYSQL; private final String UPSERT_POSTGRES; - private final CoreLogger logger; - private final SQLConfig sqlConfig; - private final boolean isPostgres; - private Connection connection; - - /** - * Immutable value object carrying the SQL connection parameters read from config. - */ - public record SQLConfig( - String host, - int port, - String database, - String username, - String password, - String driver, - String tablePrefix, - boolean useSSL, - int connectionTimeout - ) {} - - public SQLPlayerJoinTracker(CoreLogger logger, SQLConfig sqlConfig, Path pluginDataFolder) + public SQLPlayerJoinTracker(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder) throws SQLException, SQLDriverLoader.DriverLoadException { - this.logger = logger; - this.sqlConfig = sqlConfig; - this.isPostgres = "postgresql".equals(sqlConfig.driver()); - new SQLDriverLoader(logger, pluginDataFolder).ensureLoaded(sqlConfig.driver()); + super(logger, sqlConfig, dataFolder); String tableName = sqlConfig.tablePrefix() + "players_joined"; @@ -79,13 +56,17 @@ public SQLPlayerJoinTracker(CoreLogger logger, SQLConfig sqlConfig, Path pluginD "INSERT INTO " + tableName + " (player_uuid, player_name) VALUES (?, ?) " + "ON CONFLICT (player_uuid) DO UPDATE SET player_name = EXCLUDED.player_name"; - setUpConnection(); + } + + @Override + protected String createTableSql() { + return isPostgres ? CREATE_TABLE_POSTGRES : CREATE_TABLE_MYSQL; } @Override public synchronized boolean hasJoined(UUID playerUuid) { if (isConnectionInvalid()) return false; - try (PreparedStatement ps = connection.prepareStatement(SELECT_SQL)) { + try (PreparedStatement ps = connection().prepareStatement(SELECT_SQL)) { ps.setString(1, playerUuid.toString()); try (ResultSet rs = ps.executeQuery()) { return rs.next(); @@ -100,7 +81,7 @@ public synchronized boolean hasJoined(UUID playerUuid) { public synchronized void markAsJoined(UUID playerUuid, String playerName) { if (isConnectionInvalid()) return; String upsert = isPostgres ? UPSERT_POSTGRES : UPSERT_MYSQL; - try (PreparedStatement ps = connection.prepareStatement(upsert)) { + try (PreparedStatement ps = connection().prepareStatement(upsert)) { ps.setString(1, playerUuid.toString()); ps.setString(2, playerName); ps.executeUpdate(); @@ -108,68 +89,4 @@ public synchronized void markAsJoined(UUID playerUuid, String playerName) { logger.severe("[SQLPlayerJoinTracker] SQL failure marking player '" + playerName + "' (" + playerUuid + ") as joined: " + e.getMessage()); } } - - @Override - public synchronized void close() throws SQLException { - if (connection != null && !connection.isClosed()) { - connection.close(); - } - } - - // --- Internal helpers --- - - /** - * Opens a new connection and ensures the table exists. - */ - private void setUpConnection() throws SQLException { - String url = buildJdbcUrl(); - this.connection = DriverManager.getConnection(url, sqlConfig.username(), sqlConfig.password()); - try (Statement stmt = connection.createStatement()) { - stmt.execute(isPostgres ? CREATE_TABLE_POSTGRES : CREATE_TABLE_MYSQL); - } - logger.debug("[SQLPlayerJoinTracker] Connected to " + sqlConfig.driver() + " at " + sqlConfig.host() + ":" + sqlConfig.port()); - } - - /** - * Returns {@code true} if the connection is unusable, attempting a reconnect first. - */ - private boolean isConnectionInvalid() { - try { - if (connection == null || connection.isClosed() || !connection.isValid(sqlConfig.connectionTimeout())) { - logger.info("[SQLPlayerJoinTracker] Connection lost — attempting reconnect..."); - setUpConnection(); - } - return false; - } catch (SQLException e) { - logger.severe("[SQLPlayerJoinTracker] Cannot reach SQL server at '" + sqlConfig.host() + "': " + e.getMessage()); - return true; - } - } - - /** - * Builds a JDBC URL from the {@link SQLConfig}. - * - *

- */ - private String buildJdbcUrl() { - // Driver is already expected to be one of "mysql", "mariadb", or "postgresql" - StringBuilder url = new StringBuilder() - .append("jdbc:").append(sqlConfig.driver()).append("://") - .append(sqlConfig.host()).append(':').append(sqlConfig.port()) - .append('/').append(sqlConfig.database()) - .append("?autoReconnect=true") - .append("&connectTimeout=").append(sqlConfig.connectionTimeout() * 1000) - .append("&allowPublicKeyRetrieval=true"); - - if (!isPostgres) { - url.append("&useSSL=").append(sqlConfig.useSSL()); - url.append("&characterEncoding=utf8"); - } - - return url.toString(); - } }