Skip to content

Commit e4f2825

Browse files
authored
Merge pull request #46 from RagingTech/sql-storage-abstraction
2 parents 4f690d7 + bf37a34 commit e4f2825

5 files changed

Lines changed: 131 additions & 177 deletions

File tree

src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import org.bstats.charts.SimplePie;
77
import xyz.earthcow.networkjoinmessages.common.ConfigManager;
88
import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin;
9-
import xyz.earthcow.networkjoinmessages.common.storage.SQLPlayerJoinTracker;
9+
import xyz.earthcow.networkjoinmessages.common.storage.SQLConfig;
1010

1111
import java.util.*;
1212
import java.util.concurrent.ThreadLocalRandom;
@@ -308,11 +308,11 @@ private String selectMessage(String definite, List<String> pool) {
308308
// --- SQL config builder ---
309309

310310
/**
311-
* Builds a {@link SQLPlayerJoinTracker.SQLConfig} from the values loaded during {@link #reload()}.
311+
* Builds a {@link SQLConfig} from the values loaded during {@link #reload()}.
312312
* Only meaningful when {@link #getStorageType()} is {@code "SQL"}.
313313
*/
314-
public SQLPlayerJoinTracker.SQLConfig buildSqlConfig() {
315-
return new SQLPlayerJoinTracker.SQLConfig(
314+
public SQLConfig buildSqlConfig() {
315+
return new SQLConfig(
316316
sqlHost, sqlPort, sqlDatabase,
317317
sqlUsername, sqlPassword,
318318
sqlDriver, sqlTablePrefix, sqlUseSSL, sqlConnectionTimeout
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package xyz.earthcow.networkjoinmessages.common.storage;
2+
3+
/**
4+
* Immutable value object carrying the SQL connection parameters read from config.
5+
*/
6+
public record SQLConfig(
7+
String host,
8+
int port,
9+
String database,
10+
String username,
11+
String password,
12+
String driver,
13+
String tablePrefix,
14+
boolean useSSL,
15+
int connectionTimeout
16+
) {}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package xyz.earthcow.networkjoinmessages.common.storage;
2+
3+
import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger;
4+
import xyz.earthcow.networkjoinmessages.common.util.SQLDriverLoader;
5+
6+
import java.nio.file.Path;
7+
import java.sql.Connection;
8+
import java.sql.DriverManager;
9+
import java.sql.SQLException;
10+
import java.sql.Statement;
11+
12+
abstract class SQLHandler implements AutoCloseable {
13+
protected final CoreLogger logger;
14+
protected final SQLConfig sqlConfig;
15+
protected final boolean isPostgres;
16+
private final String logPrefix;
17+
private Connection connection;
18+
19+
protected SQLHandler(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder)
20+
throws SQLException, SQLDriverLoader.DriverLoadException {
21+
this.logger = logger;
22+
this.sqlConfig = sqlConfig;
23+
this.isPostgres = "postgresql".equals(sqlConfig.driver());
24+
this.logPrefix = "[" + getClass().getSimpleName() + "] ";
25+
new SQLDriverLoader(logger, dataFolder).ensureLoaded(sqlConfig.driver());
26+
setUpConnection();
27+
}
28+
29+
protected abstract String createTableSql();
30+
31+
// Provided to subclasses
32+
protected synchronized Connection connection() {
33+
return this.connection;
34+
}
35+
36+
/**
37+
* Returns {@code true} if the connection is unusable, attempting a reconnect first.
38+
*/
39+
protected synchronized boolean isConnectionInvalid() {
40+
try {
41+
if (connection == null || connection.isClosed() || !connection.isValid(sqlConfig.connectionTimeout())) {
42+
logger.info(logPrefix + "Connection lost — attempting reconnect...");
43+
setUpConnection();
44+
}
45+
return false;
46+
} catch (SQLException e) {
47+
logger.severe(logPrefix + "Cannot reach SQL server at '" + sqlConfig.host() + "': " + e.getMessage());
48+
return true;
49+
}
50+
}
51+
52+
/**
53+
* Opens a new connection and ensures the table exists.
54+
*/
55+
private void setUpConnection() throws SQLException {
56+
String url = buildJdbcUrl();
57+
this.connection = DriverManager.getConnection(url, sqlConfig.username(), sqlConfig.password());
58+
try (Statement stmt = connection.createStatement()) {
59+
stmt.execute(createTableSql());
60+
}
61+
logger.debug(logPrefix + "Connected to " + sqlConfig.driver() + " at " + sqlConfig.host() + ":" + sqlConfig.port());
62+
}
63+
64+
private String buildJdbcUrl() {
65+
StringBuilder url = new StringBuilder()
66+
.append("jdbc:").append(sqlConfig.driver()).append("://")
67+
.append(sqlConfig.host()).append(':').append(sqlConfig.port())
68+
.append('/').append(sqlConfig.database())
69+
.append("?autoReconnect=true")
70+
.append("&connectTimeout=").append(sqlConfig.connectionTimeout() * 1000)
71+
.append("&allowPublicKeyRetrieval=true");
72+
73+
if (!isPostgres) {
74+
url.append("&useSSL=").append(sqlConfig.useSSL());
75+
url.append("&characterEncoding=utf8");
76+
}
77+
78+
return url.toString();
79+
}
80+
81+
@Override
82+
public synchronized void close() throws SQLException {
83+
if (connection != null && !connection.isClosed()) {
84+
connection.close();
85+
}
86+
}
87+
88+
}

src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java

Lines changed: 13 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
*
1515
* <p>Supports MySQL, MariaDB, and PostgreSQL. The required JDBC driver JAR is
1616
* downloaded automatically from Maven Central on first use and cached in
17-
* {@code <pluginDataFolder>/drivers/}. See {@link SQLDriverLoader}.
17+
* {@code <dataFolder>/drivers/}. See {@link SQLDriverLoader}.
1818
*
19-
* <p>Connection details are supplied via {@link SQLPlayerJoinTracker.SQLConfig}.
19+
* <p>Connection details are supplied via {@link SQLConfig}.
2020
* The store keeps a single persistent {@link Connection} and transparently
2121
* reconnects on failure.
2222
*
2323
* <p>All public methods are {@code synchronized} for thread safety.
2424
*/
25-
public class SQLPlayerDataStore implements PlayerDataStore {
25+
public class SQLPlayerDataStore extends SQLHandler implements PlayerDataStore {
2626

2727
// MySQL / MariaDB uses INSERT … ON DUPLICATE KEY UPDATE.
2828
// PostgreSQL uses INSERT … ON CONFLICT DO UPDATE.
@@ -33,17 +33,9 @@ public class SQLPlayerDataStore implements PlayerDataStore {
3333
private final String UPSERT_MYSQL;
3434
private final String UPSERT_POSTGRES;
3535

36-
private final CoreLogger logger;
37-
private final SQLPlayerJoinTracker.SQLConfig sqlConfig;
38-
private final boolean isPostgres;
39-
private Connection connection;
40-
41-
public SQLPlayerDataStore(CoreLogger logger, SQLPlayerJoinTracker.SQLConfig sqlConfig, Path pluginDataFolder)
36+
public SQLPlayerDataStore(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder)
4237
throws SQLException, SQLDriverLoader.DriverLoadException {
43-
this.logger = logger;
44-
this.sqlConfig = sqlConfig;
45-
this.isPostgres = "postgresql".equals(sqlConfig.driver());
46-
new SQLDriverLoader(logger, pluginDataFolder).ensureLoaded(sqlConfig.driver());
38+
super(logger, sqlConfig, dataFolder);
4739

4840
String tableName = sqlConfig.tablePrefix() + "players";
4941

@@ -92,14 +84,18 @@ public SQLPlayerDataStore(CoreLogger logger, SQLPlayerJoinTracker.SQLConfig sqlC
9284
" ignore_swap = EXCLUDED.ignore_swap," +
9385
" ignore_leave = EXCLUDED.ignore_leave";
9486

95-
setUpConnection();
87+
}
88+
89+
@Override
90+
protected String createTableSql() {
91+
return isPostgres ? CREATE_TABLE_POSTGRES : CREATE_TABLE_MYSQL;
9692
}
9793

9894
@Override
9995
@Nullable
10096
public synchronized PlayerDataSnapshot getData(UUID playerUuid) {
10197
if (isConnectionInvalid()) return null;
102-
try (PreparedStatement ps = connection.prepareStatement(SELECT_SQL)) {
98+
try (PreparedStatement ps = connection().prepareStatement(SELECT_SQL)) {
10399
ps.setString(1, playerUuid.toString());
104100
try (ResultSet rs = ps.executeQuery()) {
105101
if (rs.next()) {
@@ -122,7 +118,7 @@ public synchronized PlayerDataSnapshot getData(UUID playerUuid) {
122118
public synchronized void saveData(UUID playerUuid, PlayerDataSnapshot data) {
123119
if (isConnectionInvalid()) return;
124120
String upsert = isPostgres ? UPSERT_POSTGRES : UPSERT_MYSQL;
125-
try (PreparedStatement ps = connection.prepareStatement(upsert)) {
121+
try (PreparedStatement ps = connection().prepareStatement(upsert)) {
126122
ps.setString(1, playerUuid.toString());
127123
ps.setString(2, data.playerName());
128124
ps.setObject(3, data.silentState(), Types.BOOLEAN);
@@ -139,7 +135,7 @@ public synchronized void saveData(UUID playerUuid, PlayerDataSnapshot data) {
139135
@Nullable
140136
public synchronized UUID resolveUuid(String playerName) {
141137
if (isConnectionInvalid()) return null;
142-
try (PreparedStatement ps = connection.prepareStatement(RESOLVE_SQL)) {
138+
try (PreparedStatement ps = connection().prepareStatement(RESOLVE_SQL)) {
143139
ps.setString(1, playerName);
144140
try (ResultSet rs = ps.executeQuery()) {
145141
if (rs.next()) {
@@ -151,67 +147,4 @@ public synchronized UUID resolveUuid(String playerName) {
151147
}
152148
return null;
153149
}
154-
155-
@Override
156-
public synchronized void close() throws SQLException {
157-
if (connection != null && !connection.isClosed()) {
158-
connection.close();
159-
}
160-
}
161-
162-
// --- Internal helpers ---
163-
164-
/**
165-
* Opens a new connection and ensures the table exists.
166-
*/
167-
private void setUpConnection() throws SQLException {
168-
String url = buildJdbcUrl();
169-
this.connection = DriverManager.getConnection(url, sqlConfig.username(), sqlConfig.password());
170-
try (Statement stmt = connection.createStatement()) {
171-
stmt.execute(isPostgres ? CREATE_TABLE_POSTGRES : CREATE_TABLE_MYSQL);
172-
}
173-
logger.debug("[SQLPlayerDataStore] Connected to " + sqlConfig.driver() + " at " + sqlConfig.host() + ":" + sqlConfig.port());
174-
}
175-
176-
/**
177-
* Returns {@code true} if the connection is unusable, attempting a reconnect first.
178-
*/
179-
private boolean isConnectionInvalid() {
180-
try {
181-
if (connection == null || connection.isClosed() || !connection.isValid(sqlConfig.connectionTimeout())) {
182-
logger.info("[SQLPlayerDataStore] Connection lost — attempting reconnect...");
183-
setUpConnection();
184-
}
185-
return false;
186-
} catch (SQLException e) {
187-
logger.severe("[SQLPlayerDataStore] Cannot reach SQL server at '" + sqlConfig.host() + "': " + e.getMessage());
188-
return true;
189-
}
190-
}
191-
192-
/**
193-
* Builds a JDBC URL from the {@link SQLPlayerJoinTracker.SQLConfig}.
194-
*
195-
* <ul>
196-
* <li>MySQL: {@code jdbc:mysql://host:port/db?...}</li>
197-
* <li>MariaDB: {@code jdbc:mariadb://host:port/db?...}</li>
198-
* <li>PostgreSQL: {@code jdbc:postgresql://host:port/db?...}</li>
199-
* </ul>
200-
*/
201-
private String buildJdbcUrl() {
202-
StringBuilder url = new StringBuilder()
203-
.append("jdbc:").append(sqlConfig.driver()).append("://")
204-
.append(sqlConfig.host()).append(':').append(sqlConfig.port())
205-
.append('/').append(sqlConfig.database())
206-
.append("?autoReconnect=true")
207-
.append("&connectTimeout=").append(sqlConfig.connectionTimeout() * 1000)
208-
.append("&allowPublicKeyRetrieval=true");
209-
210-
if (!isPostgres) {
211-
url.append("&useSSL=").append(sqlConfig.useSSL());
212-
url.append("&characterEncoding=utf8");
213-
}
214-
215-
return url.toString();
216-
}
217150
}

0 commit comments

Comments
 (0)