diff --git a/src/main/java/net/ardakaz/griefalert/GriefAlert.java b/src/main/java/net/ardakaz/griefalert/GriefAlert.java index 59de1e5..e5b6122 100644 --- a/src/main/java/net/ardakaz/griefalert/GriefAlert.java +++ b/src/main/java/net/ardakaz/griefalert/GriefAlert.java @@ -1,5 +1,9 @@ package net.ardakaz.griefalert; +import net.ardakaz.griefalert.database.DatabaseService; +import net.ardakaz.griefalert.database.Migrations; +import net.ardakaz.griefalert.database.PlayerRepository; +import net.ardakaz.griefalert.listener.PlayerListener; import net.coreprotect.CoreProtect; import net.coreprotect.CoreProtectAPI; import net.coreprotect.CoreProtectAPI.ParseResult; @@ -28,6 +32,7 @@ import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import java.sql.*; @@ -35,6 +40,10 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; import java.util.stream.Collectors; @@ -48,6 +57,8 @@ public class GriefAlert extends JavaPlugin implements Listener { private String MAP_LINK; private Boolean ALLOW_STEALING; private Connection connection; + private DatabaseService database; + private Logger logger; /* UTILITY FUNCTIONS @@ -155,6 +166,7 @@ public class GriefAlert extends JavaPlugin implements Listener { // Init GriefAlert @Override public void onEnable() { + logger = getLogger(); coreProtectAPI = getCoreProtect(); if (coreProtectAPI == null) { @@ -163,7 +175,6 @@ public class GriefAlert extends JavaPlugin implements Listener { return; } - getServer().getPluginManager().registerEvents(this, this); // Config saveDefaultConfig(); @@ -176,9 +187,18 @@ public class GriefAlert extends JavaPlugin implements Listener { setupDatabase(); + PlayerRepository playerRepository = new PlayerRepository(database, logger); + PlayerListener playerListener = new PlayerListener(playerRepository, logger); + + + PluginManager pluginManager = getServer().getPluginManager(); + pluginManager.registerEvents(this, this); + pluginManager.registerEvents(playerListener, this); + getCommand("griefalert").setTabCompleter(this); getLogger().info("GriefAlert has been enabled."); + } @Override @@ -193,19 +213,19 @@ public class GriefAlert extends JavaPlugin implements Listener { } private void setupDatabase() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + String jdbcUrl = getConfig().getString("jdbc-url"); try { connection = DriverManager.getConnection(jdbcUrl); - Statement stmt = connection.createStatement(); - stmt.executeUpdate(Migrations.CREATE_TABLE_IGNORED_LOCATIONS); - stmt.executeUpdate(Migrations.CREATE_TABLE_PLAYERS); - stmt.executeUpdate(Migrations.CREATE_TABLE_ACTIONS); - stmt.executeUpdate(Migrations.CREATE_TABLE_ALERTS); - stmt.executeUpdate(Migrations.CREATE_TABLE_REPORTS); - stmt.close(); - getLogger().info("Connected to "+jdbcUrl); + database = new DatabaseService(executor, connection); + logger.info("Connected to " + jdbcUrl); + + CompletableFuture migrationExecution = new Migrations(database, logger).execute(); + migrationExecution.join(); + logger.info("Database migration finished"); } catch (SQLException e) { - getLogger().severe("Could not set up SQLite database: " + e.getMessage()); + logger.severe("Could not set up SQLite database: " + e.getMessage()); } } diff --git a/src/main/java/net/ardakaz/griefalert/database/DatabaseOperation.java b/src/main/java/net/ardakaz/griefalert/database/DatabaseOperation.java new file mode 100644 index 0000000..a042547 --- /dev/null +++ b/src/main/java/net/ardakaz/griefalert/database/DatabaseOperation.java @@ -0,0 +1,9 @@ +package net.ardakaz.griefalert.database; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface DatabaseOperation { + T execute(Connection connection) throws SQLException; +} \ No newline at end of file diff --git a/src/main/java/net/ardakaz/griefalert/database/DatabaseService.java b/src/main/java/net/ardakaz/griefalert/database/DatabaseService.java new file mode 100644 index 0000000..2ed434d --- /dev/null +++ b/src/main/java/net/ardakaz/griefalert/database/DatabaseService.java @@ -0,0 +1,27 @@ +package net.ardakaz.griefalert.database; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +public class DatabaseService { + + private final ExecutorService executor; + private final Connection connection; + + public DatabaseService(ExecutorService executor, Connection connection) { + this.executor = executor; + this.connection = connection; + } + + public CompletableFuture submit(DatabaseOperation operation) { + return CompletableFuture.supplyAsync(() -> { + try { + return operation.execute(connection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }, executor); + } +} \ No newline at end of file diff --git a/src/main/java/net/ardakaz/griefalert/Migrations.java b/src/main/java/net/ardakaz/griefalert/database/Migrations.java similarity index 61% rename from src/main/java/net/ardakaz/griefalert/Migrations.java rename to src/main/java/net/ardakaz/griefalert/database/Migrations.java index 455db02..7f4feb4 100644 --- a/src/main/java/net/ardakaz/griefalert/Migrations.java +++ b/src/main/java/net/ardakaz/griefalert/database/Migrations.java @@ -1,8 +1,21 @@ -package net.ardakaz.griefalert; +package net.ardakaz.griefalert.database; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; public class Migrations { - public static final String CREATE_TABLE_IGNORED_LOCATIONS = + private DatabaseService database; + private Logger logger; + + public Migrations(DatabaseService database, Logger logger) { + this.database = database; + this.logger = logger; + } + + private static final String CREATE_TABLE_IGNORED_LOCATIONS = "CREATE TABLE IF NOT EXISTS ignored_locations (" + "x INTEGER, " + "y INTEGER, " + @@ -11,16 +24,16 @@ public class Migrations { "PRIMARY KEY (x, y, z, world)" + ")"; - public static final String CREATE_TABLE_PLAYERS = + private static final String CREATE_TABLE_PLAYERS = "CREATE TABLE IF NOT EXISTS players (" + "id TEXT PRIMARY KEY, " + "names TEXT NOT NULL, " + - "playtime INTEGER NOT NULL DEFAULT 0, " + - "firstseen INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " + - "lastseen INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP" + + "firstseen INTEGER NOT NULL, " + + "lastseen INTEGER NOT NULL, " + + "playtime INTEGER NOT NULL DEFAULT 0 " + ")"; - public static final String CREATE_TABLE_ACTIONS = + private static final String CREATE_TABLE_ACTIONS = "CREATE TABLE IF NOT EXISTS actions (" + "id INTEGER PRIMARY KEY, " + "player TEXT NOT NULL, " + @@ -32,7 +45,7 @@ public class Migrations { "FOREIGN KEY(player) REFERENCES players(id)" + ")"; - public static final String CREATE_TABLE_ALERTS = + private static final String CREATE_TABLE_ALERTS = "CREATE TABLE IF NOT EXISTS alerts (" + "id INTEGER PRIMARY KEY, " + "player TEXT NOT NULL, " + @@ -52,7 +65,7 @@ public class Migrations { "FOREIGN KEY(player) REFERENCES players(id)" + ")"; - public static final String CREATE_TABLE_REPORTS = + private static final String CREATE_TABLE_REPORTS = "CREATE TABLE IF NOT EXISTS reports (" + "id INTEGER PRIMARY KEY, " + "player TEXT NOT NULL, " + @@ -65,4 +78,19 @@ public class Migrations { "timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "state TEXT NOT NULL DEFAULT 'UNRESOLVED'" + ")"; + + public CompletableFuture execute() { + return database.submit(connection -> { + try(Statement stmt = connection.createStatement()){ + stmt.executeUpdate(Migrations.CREATE_TABLE_IGNORED_LOCATIONS); + stmt.executeUpdate(Migrations.CREATE_TABLE_PLAYERS); + stmt.executeUpdate(Migrations.CREATE_TABLE_ACTIONS); + stmt.executeUpdate(Migrations.CREATE_TABLE_ALERTS); + stmt.executeUpdate(Migrations.CREATE_TABLE_REPORTS); + } catch (SQLException e) { + logger.severe("Failed to execute database migrations: " + e.getMessage()); + } + return null; + }); + } } diff --git a/src/main/java/net/ardakaz/griefalert/database/PlayerEntity.java b/src/main/java/net/ardakaz/griefalert/database/PlayerEntity.java new file mode 100644 index 0000000..8fd9a3e --- /dev/null +++ b/src/main/java/net/ardakaz/griefalert/database/PlayerEntity.java @@ -0,0 +1,6 @@ +package net.ardakaz.griefalert.database; + +import java.util.List; + +public record PlayerEntity(String id, List names, long playtime, long firstSeen, long lastSeen) { +} \ No newline at end of file diff --git a/src/main/java/net/ardakaz/griefalert/database/PlayerRepository.java b/src/main/java/net/ardakaz/griefalert/database/PlayerRepository.java new file mode 100644 index 0000000..3f3b98c --- /dev/null +++ b/src/main/java/net/ardakaz/griefalert/database/PlayerRepository.java @@ -0,0 +1,95 @@ +package net.ardakaz.griefalert.database; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; + +public class PlayerRepository { + private Logger logger; + private final DatabaseService database; + + public PlayerRepository(DatabaseService database, Logger logger) { + this.database = database; + this.logger = logger; + } + + public CompletableFuture get(String uuid) { + return database.submit(connection -> { + + try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM players WHERE id=?")) { + ps.setString(1, uuid); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new PlayerEntity( + rs.getString("id"), + Arrays.stream(rs.getString("names").split(",")).toList(), + rs.getLong("playtime"), + rs.getLong("firstseen"), + rs.getLong("lastseen") + ); + } + } + } catch (SQLException e) { + logger.severe("DB error: " + e.getMessage()); + } + return null; + + }); + } + + public CompletableFuture upsert(PlayerEntity entity) { + return database.submit(connection -> { + String upsert = """ + INSERT INTO players(id, names, firstseen, lastseen) VALUES(?,?,?,?) + ON CONFLICT(id) + DO UPDATE SET names=excluded.names, playtime=?, lastseen=excluded.lastseen; + """; + + try (PreparedStatement ps = connection.prepareStatement(upsert)) { + ps.setString(1, entity.id()); + String names = String.join(",", entity.names()); + ps.setString(2, names); + ps.setLong(3, entity.firstSeen()); + ps.setLong(4, entity.lastSeen()); + ps.setLong(5, entity.playtime()); + + ps.executeUpdate(); + + } catch (SQLException e) { + logger.severe("Failed to save player: " + entity); + logger.severe(e.getMessage()); + } + + return null; + }); + } + + public CompletableFuture updateNames(String id, List names) { + return database.submit(connection -> { + String update = """ + UPDATE players + SET names = ? + WHERE id = ? + """; + + try (PreparedStatement ps = connection.prepareStatement(update)) { + ps.setString(1, String.join(",", names)); + ps.setString(2, id); + + ps.executeUpdate(); + + } catch (SQLException e) { + logger.severe("Failed to update names for playerID: " + id); + logger.severe(e.getMessage()); + } + + return null; + }); + } +} + diff --git a/src/main/java/net/ardakaz/griefalert/listener/PlayerListener.java b/src/main/java/net/ardakaz/griefalert/listener/PlayerListener.java new file mode 100644 index 0000000..48e7051 --- /dev/null +++ b/src/main/java/net/ardakaz/griefalert/listener/PlayerListener.java @@ -0,0 +1,77 @@ +package net.ardakaz.griefalert.listener; + +import net.ardakaz.griefalert.database.PlayerEntity; +import net.ardakaz.griefalert.database.PlayerRepository; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +public class PlayerListener implements Listener { + + private Logger logger; + private PlayerRepository repository; + + public PlayerListener(PlayerRepository repository, Logger logger) { + this.logger = logger; + this.repository = repository; + } + + @EventHandler + public void onPlayerLogin(PlayerJoinEvent event) { + Player eventPlayer = event.getPlayer(); + repository.get( + eventPlayer.getUniqueId().toString() + ).thenAccept(dbPlayer -> { + if (dbPlayer == null) { + long now = System.currentTimeMillis(); + repository.upsert(new PlayerEntity( + eventPlayer.getUniqueId().toString(), + List.of(eventPlayer.getName()), + 0, + now, + now + ) + ); + } else { + List newNames = updateNames(eventPlayer.getName(), dbPlayer.names()); + repository.updateNames(dbPlayer.id(), newNames); + } + }); + } + + @EventHandler + public void onPlayerLogout(PlayerQuitEvent event) { + Player eventPlayer = event.getPlayer(); + long playtimeSeconds = eventPlayer.getStatistic(Statistic.PLAY_ONE_MINUTE) / 20L; + repository.get( + eventPlayer.getUniqueId().toString() + ).thenAccept(dbPlayer -> { + if (dbPlayer != null) { + PlayerEntity updatedPlayer = new PlayerEntity( + dbPlayer.id(), + dbPlayer.names(), + playtimeSeconds, + dbPlayer.firstSeen(), + System.currentTimeMillis() + ); + repository.upsert(updatedPlayer); + } + }); + } + + private List updateNames(String currentName, List historicalNames) { + List newNames = new ArrayList<>(historicalNames); + while (newNames.contains(currentName)) { + newNames.remove(currentName); + } + newNames.add(currentName); + return newNames; + } +}