diff --git a/build.gradle.kts b/build.gradle.kts index 85ad6d6..49b5664 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "dev.plex" -version = "1.7-SNAPSHOT" +version = "2.0-SNAPSHOT" description = "Plex" subprojects { diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 077195d..19078fd 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -23,7 +23,10 @@ dependencies { library("commons-io:commons-io:2.22.0") library("redis.clients:jedis:7.5.0") library("org.mariadb.jdbc:mariadb-java-client:3.5.8") + library("org.postgresql:postgresql:42.7.11") + library("org.xerial:sqlite-jdbc:3.53.1.0") library("com.zaxxer:HikariCP:7.0.2") + library("com.j256.ormlite:ormlite-jdbc:6.1") library("org.jetbrains:annotations:26.1.0") compileOnly("io.papermc.paper:paper-api:26.1.2.build.+") compileOnly("com.github.MilkBowl:VaultAPI:1.7.1") { diff --git a/server/src/main/java/dev/plex/Plex.java b/server/src/main/java/dev/plex/Plex.java index 30a3125..5b8d076 100644 --- a/server/src/main/java/dev/plex/Plex.java +++ b/server/src/main/java/dev/plex/Plex.java @@ -112,7 +112,7 @@ public class Plex extends JavaPlugin playerCache = new PlayerCache(); - PlexLog.log("Attempting to connect to DB: {0}", plugin.config.getString("data.central.db")); + PlexLog.log("Attempting to connect to DB: {0}", plugin.config.getString("data.db.name")); try { PlexUtils.testConnections(); @@ -226,6 +226,11 @@ public class Plex extends JavaPlugin this.getServer().getMessenger().unregisterOutgoingPluginChannel(this); moduleManager.disableModules(); + + if (sqlConnection != null) + { + sqlConnection.close(); + } } private void generateWorlds() diff --git a/server/src/main/java/dev/plex/config/Config.java b/server/src/main/java/dev/plex/config/Config.java index a0db47d..0422fc5 100644 --- a/server/src/main/java/dev/plex/config/Config.java +++ b/server/src/main/java/dev/plex/config/Config.java @@ -4,8 +4,6 @@ import dev.plex.Plex; import dev.plex.util.PlexLog; import java.io.File; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import org.bukkit.configuration.file.YamlConfiguration; @@ -17,22 +15,17 @@ public class Config extends YamlConfiguration /** * The plugin instance */ - private Plex plugin; + private final Plex plugin; /** * The File instance */ - private File file; + private final File file; /** * The file name */ - private String name; - - /** - * Whether new entries were added to the file automatically - */ - private boolean added = false; + private final String name; /** * Creates a config object @@ -60,37 +53,20 @@ public class Config extends YamlConfiguration /** * Loads the configuration file */ - public void load(boolean loadFromFile) + public void load(boolean reconcileWithDefaults) { try { - if (loadFromFile) + if (reconcileWithDefaults) { - YamlConfiguration externalYamlConfig = YamlConfiguration.loadConfiguration(file); - InputStreamReader internalConfigFileStream = new InputStreamReader(plugin.getResource(name), StandardCharsets.UTF_8); - YamlConfiguration internalYamlConfig = YamlConfiguration.loadConfiguration(internalConfigFileStream); - - // Gets all the keys inside the internal file and iterates through all of it's key pairs - for (String string : internalYamlConfig.getKeys(true)) + ConfigDefaultsMerger.Result result = ConfigDefaultsMerger.merge(file, plugin.getResource(name), name); + if (!result.addedKeys().isEmpty()) { - // Checks if the external file contains the key already. - if (!externalYamlConfig.contains(string)) - { - // If it doesn't contain the key, we set the key based off what was found inside the plugin jar - externalYamlConfig.setComments(string, internalYamlConfig.getComments(string)); - externalYamlConfig.setInlineComments(string, internalYamlConfig.getInlineComments(string)); - externalYamlConfig.set(string, internalYamlConfig.get(string)); - PlexLog.log("Setting key: " + string + " in " + this.name + " to the default value(s) since it does not exist!"); - added = true; - } - } - if (added) - { - externalYamlConfig.save(file); - PlexLog.log("Saving new file..."); - added = false; + PlexLog.log("Merged default key(s) into " + name + ": " + String.join(", ", result.addedKeys())); } } + + this.options().parseComments(true); super.load(file); } catch (Exception ex) @@ -121,4 +97,4 @@ public class Config extends YamlConfiguration { plugin.saveResource(name, false); } -} \ No newline at end of file +} diff --git a/server/src/main/java/dev/plex/config/ConfigDefaultsMerger.java b/server/src/main/java/dev/plex/config/ConfigDefaultsMerger.java new file mode 100644 index 0000000..2638782 --- /dev/null +++ b/server/src/main/java/dev/plex/config/ConfigDefaultsMerger.java @@ -0,0 +1,220 @@ +package dev.plex.config; + +import dev.plex.util.PlexLog; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class ConfigDefaultsMerger +{ + private static final Pattern KEY = Pattern.compile("^(\\s*)([A-Za-z0-9_-]+):(?:\\s+.*)?$"); + + private ConfigDefaultsMerger() + { + } + + static Result merge(File file, InputStream defaultsStream, String displayName) throws IOException, InvalidConfigurationException + { + if (defaultsStream == null) + { + PlexLog.warn("Unable to merge defaults into " + displayName + " because no default resource exists."); + return new Result(load(file), List.of(), false); + } + + String defaultsText = new String(defaultsStream.readAllBytes(), StandardCharsets.UTF_8); + List defaultLines = splitLines(defaultsText); + List currentLines = splitLines(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + Map defaults = parse(defaultLines); + Map current = parse(currentLines); + + List insertions = new ArrayList<>(); + Set coveredByMissingParent = new HashSet<>(); + + int order = 0; + for (Entry entry : defaults.values()) + { + if (current.containsKey(entry.path) || isCovered(entry.path, coveredByMissingParent)) + { + order++; + continue; + } + + String parent = parent(entry.path); + if (parent != null && !current.containsKey(parent)) + { + order++; + continue; + } + + insertions.add(new Insertion(findInsertionIndex(entry, defaults, current, currentLines.size()), order, entry.path, defaultLines.subList(entry.start, entry.end))); + coveredByMissingParent.add(entry.path); + order++; + } + + if (insertions.isEmpty()) + { + return new Result(load(file), List.of(), false); + } + + applyInsertions(currentLines, insertions); + Files.writeString(file.toPath(), String.join(System.lineSeparator(), currentLines) + System.lineSeparator(), StandardCharsets.UTF_8); + return new Result(load(file), insertions.stream().map(Insertion::path).toList(), true); + } + + private static int findInsertionIndex(Entry missing, Map defaults, Map current, int fallback) + { + String parent = parent(missing.path); + for (Entry candidate : defaults.values()) + { + if (candidate.start <= missing.start || !sameParent(missing.path, candidate.path)) + { + continue; + } + Entry existingCandidate = current.get(candidate.path); + if (existingCandidate != null) + { + return existingCandidate.start; + } + } + + if (parent != null && current.containsKey(parent)) + { + return current.get(parent).end; + } + return fallback; + } + + private static void applyInsertions(List currentLines, List insertions) + { + insertions.sort(Comparator.comparingInt(Insertion::index).reversed().thenComparing(Comparator.comparingInt(Insertion::order).reversed())); + for (Insertion insertion : insertions) + { + List block = new ArrayList<>(insertion.lines); + if (insertion.index > 0 && !currentLines.get(insertion.index - 1).isBlank() && !block.get(0).isBlank()) + { + block.add(0, ""); + } + if (insertion.index < currentLines.size() && !currentLines.get(insertion.index).isBlank() && !block.get(block.size() - 1).isBlank()) + { + block.add(""); + } + currentLines.addAll(insertion.index, block); + } + } + + private static Map parse(List lines) + { + Map entries = new LinkedHashMap<>(); + List stack = new ArrayList<>(); + + for (int i = 0; i < lines.size(); i++) + { + Matcher matcher = KEY.matcher(lines.get(i)); + if (!matcher.matches() || lines.get(i).trim().startsWith("#")) + { + continue; + } + + int indent = matcher.group(1).length(); + String key = matcher.group(2); + while (!stack.isEmpty() && stack.get(stack.size() - 1).indent >= indent) + { + stack.remove(stack.size() - 1); + } + + String path = stack.isEmpty() ? key : stack.get(stack.size() - 1).path + "." + key; + entries.put(path, new Entry(path, blockStart(lines, i), blockEnd(lines, i, indent), indent)); + stack.add(new StackEntry(path, indent)); + } + return entries; + } + + private static int blockStart(List lines, int keyLine) + { + int start = keyLine; + while (start > 0) + { + String previous = lines.get(start - 1).trim(); + if (!previous.isBlank() && !previous.startsWith("#")) + { + break; + } + start--; + } + return start; + } + + private static int blockEnd(List lines, int keyLine, int indent) + { + for (int i = keyLine + 1; i < lines.size(); i++) + { + Matcher matcher = KEY.matcher(lines.get(i)); + if (matcher.matches() && matcher.group(1).length() <= indent) + { + return blockStart(lines, i); + } + } + return lines.size(); + } + + private static boolean sameParent(String first, String second) + { + String firstParent = parent(first); + String secondParent = parent(second); + return firstParent == null ? secondParent == null : firstParent.equals(secondParent); + } + + private static boolean isCovered(String path, Set covered) + { + return covered.stream().anyMatch(parent -> path.startsWith(parent + ".")); + } + + private static String parent(String path) + { + int index = path.lastIndexOf('.'); + return index == -1 ? null : path.substring(0, index); + } + + private static List splitLines(String text) + { + return new ArrayList<>(text.lines().toList()); + } + + static YamlConfiguration load(File file) throws IOException, InvalidConfigurationException + { + YamlConfiguration config = new YamlConfiguration(); + config.options().parseComments(true); + config.load(file); + return config; + } + + private record StackEntry(String path, int indent) + { + } + + private record Entry(String path, int start, int end, int indent) + { + } + + private record Insertion(int index, int order, String path, List lines) + { + } + + record Result(YamlConfiguration config, List addedKeys, boolean changed) + { + } +} diff --git a/server/src/main/java/dev/plex/config/ModuleConfig.java b/server/src/main/java/dev/plex/config/ModuleConfig.java index ed74a56..8993477 100644 --- a/server/src/main/java/dev/plex/config/ModuleConfig.java +++ b/server/src/main/java/dev/plex/config/ModuleConfig.java @@ -1,9 +1,11 @@ package dev.plex.config; import dev.plex.module.PlexModule; +import dev.plex.util.PlexLog; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import org.bukkit.configuration.InvalidConfigurationException; @@ -17,22 +19,22 @@ public class ModuleConfig extends YamlConfiguration /** * The plugin instance */ - private PlexModule module; + private final PlexModule module; /** * The File instance */ - private File file; + private final File file; /** * Where the file is in the module JAR */ - private String from; + private final String from; /** * Where it should be copied to in the module folder */ - private String to; + private final String to; /** * Creates a config object @@ -57,6 +59,13 @@ public class ModuleConfig extends YamlConfiguration { try { + ConfigDefaultsMerger.Result result = ConfigDefaultsMerger.merge(file, module.getClass().getResourceAsStream("/" + from), to); + if (!result.addedKeys().isEmpty()) + { + PlexLog.log("Merged default key(s) into " + to + ": " + String.join(", ", result.addedKeys())); + } + + this.options().parseComments(true); super.load(file); } catch (IOException | InvalidConfigurationException ex) @@ -87,11 +96,24 @@ public class ModuleConfig extends YamlConfiguration { try { - Files.copy(module.getClass().getResourceAsStream("/" + from), this.file.toPath()); + File parent = file.getParentFile(); + if (parent != null) + { + parent.mkdirs(); + } + try (InputStream stream = module.getClass().getResourceAsStream("/" + from)) + { + if (stream == null) + { + PlexLog.warn("Unable to save default module config " + to + ": missing resource " + from); + return; + } + Files.copy(stream, this.file.toPath()); + } } catch (IOException e) { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java b/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java index a3d6ae9..61de625 100644 --- a/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java +++ b/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java @@ -28,7 +28,7 @@ public class PunishedPlayerMenu extends PageableMenu @Override protected ItemStack toItem(Punishment object) { - return new ItemBuilder(Material.PAPER).displayName("" + object.getType().name()).lore("By: " + (object.getPunisher() == null ? "CONSOLE" : Plex.get().getSqlPlayerData().getNameByUUID(object.getPunisher())), "Expire(d/s): " + TimeUtils.useTimezone(object.getEndDate()), "Reason: " + object.getReason()).build(); + return new ItemBuilder(Material.PAPER).displayName("" + object.getType().name()).lore("By: " + (object.getPunisher() == null ? "CONSOLE" : Plex.get().getSqlPlayerData().getNameByUUID(object.getPunisher())), "Issued: " + TimeUtils.useTimezone(object.getIssueDate()), "Expire(d/s): " + TimeUtils.useTimezone(object.getEndDate()), "Reason: " + object.getReason()).build(); } @Override diff --git a/server/src/main/java/dev/plex/player/PlexPlayer.java b/server/src/main/java/dev/plex/player/PlexPlayer.java index 0d7600e..604f903 100644 --- a/server/src/main/java/dev/plex/player/PlexPlayer.java +++ b/server/src/main/java/dev/plex/player/PlexPlayer.java @@ -6,10 +6,6 @@ import dev.plex.Plex; import dev.plex.punishment.Punishment; import dev.plex.punishment.PunishmentType; import dev.plex.punishment.extra.Note; -import dev.plex.storage.annotation.MapObjectList; -import dev.plex.storage.annotation.PrimaryKey; -import dev.plex.storage.annotation.SQLTable; -import dev.plex.storage.annotation.VarcharLimit; import dev.plex.util.adapter.ZonedDateTimeAdapter; import java.time.ZonedDateTime; @@ -26,15 +22,12 @@ import org.jetbrains.annotations.NotNull; @Getter @Setter -@SQLTable("players") public class PlexPlayer { @Setter(AccessLevel.NONE) - @PrimaryKey @NotNull private UUID uuid; - @VarcharLimit(16) @NotNull private String name; @@ -54,10 +47,8 @@ public class PlexPlayer private List ips = Lists.newArrayList(); - @MapObjectList private List punishments = Lists.newArrayList(); - @MapObjectList private List notes = Lists.newArrayList(); public PlexPlayer() diff --git a/server/src/main/java/dev/plex/punishment/Punishment.java b/server/src/main/java/dev/plex/punishment/Punishment.java index a38bc18..4ef9665 100644 --- a/server/src/main/java/dev/plex/punishment/Punishment.java +++ b/server/src/main/java/dev/plex/punishment/Punishment.java @@ -3,11 +3,11 @@ package dev.plex.punishment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import dev.plex.Plex; -import dev.plex.storage.annotation.SQLTable; import dev.plex.util.PlexUtils; import dev.plex.util.TimeUtils; import dev.plex.util.adapter.ZonedDateTimeAdapter; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.UUID; @@ -18,7 +18,6 @@ import org.jetbrains.annotations.NotNull; @Getter @Setter -@SQLTable("punishments") public class Punishment { private static final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()).create(); @@ -36,12 +35,14 @@ public class Punishment private String reason; private boolean customTime; private boolean active; // Field is only for bans + private ZonedDateTime issueDate; private ZonedDateTime endDate; public Punishment(UUID punished, UUID punisher) { this.punished = punished; this.punisher = punisher; + this.issueDate = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)); } public static Component generateBanMessage(Punishment punishment) diff --git a/server/src/main/java/dev/plex/punishment/extra/Note.java b/server/src/main/java/dev/plex/punishment/extra/Note.java index 9d281d2..3f8b7d1 100644 --- a/server/src/main/java/dev/plex/punishment/extra/Note.java +++ b/server/src/main/java/dev/plex/punishment/extra/Note.java @@ -1,8 +1,6 @@ package dev.plex.punishment.extra; import com.google.gson.GsonBuilder; -import dev.plex.storage.annotation.NoLimit; -import dev.plex.storage.annotation.SQLTable; import dev.plex.util.adapter.ZonedDateTimeAdapter; import java.time.ZonedDateTime; @@ -11,12 +9,10 @@ import java.util.UUID; import lombok.Data; @Data -@SQLTable("notes") public class Note { private final UUID uuid; - @NoLimit private final String note; private final UUID writtenBy; private final ZonedDateTime timestamp; diff --git a/server/src/main/java/dev/plex/storage/RedisConnection.java b/server/src/main/java/dev/plex/storage/RedisConnection.java index b4d61c0..2ad8fef 100644 --- a/server/src/main/java/dev/plex/storage/RedisConnection.java +++ b/server/src/main/java/dev/plex/storage/RedisConnection.java @@ -14,11 +14,11 @@ public class RedisConnection implements PlexBase { try { - jedis = new Jedis(plugin.config.getString("data.side.hostname"), - plugin.config.getInt("data.side.port")); - if (plugin.config.getBoolean("data.side.auth")) + jedis = new Jedis(plugin.config.getString("data.redis.hostname"), + plugin.config.getInt("data.redis.port")); + if (plugin.config.getBoolean("data.redis.auth")) { - jedis.auth(plugin.config.getString("data.side.password")); + jedis.auth(plugin.config.getString("data.redis.password")); } return jedis; } @@ -43,6 +43,6 @@ public class RedisConnection implements PlexBase public final boolean isEnabled() { - return plugin.config.getBoolean("data.side.enabled"); + return plugin.config.getBoolean("data.redis.enabled"); } } diff --git a/server/src/main/java/dev/plex/storage/SQLConnection.java b/server/src/main/java/dev/plex/storage/SQLConnection.java index f4e25e1..bdca9a6 100644 --- a/server/src/main/java/dev/plex/storage/SQLConnection.java +++ b/server/src/main/java/dev/plex/storage/SQLConnection.java @@ -1,144 +1,16 @@ package dev.plex.storage; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import dev.plex.Plex; -import dev.plex.PlexBase; -import lombok.Getter; +import dev.plex.storage.database.Database; -import java.io.File; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -@Getter -public class SQLConnection implements PlexBase +/** + * Database bootstrap and connection holder. + * + *

The historical name is kept so existing module-facing accessors do not break.

+ */ +public class SQLConnection extends Database { - private HikariDataSource dataSource; - - public SQLConnection() + public java.sql.Connection getCon() throws java.sql.SQLException { - if (!plugin.config.getString("data.central.storage").equalsIgnoreCase("sqlite") && !plugin.config.getString("data.central.storage").equalsIgnoreCase("mariadb")) - { - return; - } - - String host = plugin.config.getString("data.central.hostname"); - int port = plugin.config.getInt("data.central.port"); - String username = plugin.config.getString("data.central.user"); - String password = plugin.config.getString("data.central.password"); - String database = plugin.config.getString("data.central.db"); - - HikariConfig config = new HikariConfig(); - config.addDataSourceProperty("cachePrepStmts", "true"); - config.addDataSourceProperty("prepStmtCacheSize", "250"); - config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - - this.dataSource = new HikariDataSource(); - dataSource.setMaxLifetime(15000); - dataSource.setIdleTimeout(15000 * 2); - dataSource.setConnectionTimeout(15000 * 4); - dataSource.setMinimumIdle(2); - dataSource.setMaximumPoolSize(10); - try - { - if (plugin.config.getString("data.central.storage").equalsIgnoreCase("sqlite")) - { - dataSource.setJdbcUrl("jdbc:sqlite:" + new File(plugin.getDataFolder(), "database.db").getAbsolutePath()); - plugin.setStorageType(StorageType.SQLITE); - } - else if (plugin.config.getString("data.central.storage").equalsIgnoreCase("mariadb")) - { - Class.forName("org.mariadb.jdbc.Driver"); - dataSource.setJdbcUrl("jdbc:mariadb://" + host + ":" + port + "/" + database); - dataSource.setUsername(username); - dataSource.setPassword(password); - Plex.get().setStorageType(StorageType.MARIADB); - } - } - catch (ClassNotFoundException throwables) - { - throwables.printStackTrace(); - } - - try (Connection con = getCon()) - { - if (tableExistsSQL("players")) - { - - } - con.prepareStatement("CREATE TABLE IF NOT EXISTS `players` (" + - "`uuid` VARCHAR(46) NOT NULL, " + - "`name` VARCHAR(18), " + - "`login_msg` VARCHAR(2000), " + - "`prefix` VARCHAR(2000), " + - "`staffChat` BOOLEAN, " + - "`ips` VARCHAR(2000), " + - "`coins` BIGINT, " + - "`vanished` BOOLEAN, " + - "`commandspy` BOOLEAN, " + - "PRIMARY KEY (`uuid`));").execute(); - con.prepareStatement("CREATE TABLE IF NOT EXISTS `punishments` (" + - "`punished` VARCHAR(46) NOT NULL, " + - "`punisher` VARCHAR(46), " + - "`punisherName` VARCHAR(64), " + - "`punishedUsername` VARCHAR(16), " + - "`ip` VARCHAR(2000), " + - "`type` VARCHAR(30), " + - "`reason` VARCHAR(2000), " + - "`customTime` BOOLEAN, " + - "`active` BOOLEAN, " + - "`endDate` BIGINT" + - ");").execute(); - con.prepareStatement("CREATE TABLE IF NOT EXISTS `notes` (" + - "`id` INT NOT NULL, " + - "`uuid` VARCHAR(46) NOT NULL, " + - "`written_by` VARCHAR(46), " + - "`note` VARCHAR(2000), " + - "`timestamp` BIGINT" + - ");").execute(); - } - catch (SQLException throwables) - { - throwables.printStackTrace(); - } - } - - private boolean tableExistsSQL(String tableName) throws SQLException - { - try (Connection connection = getCon()) - { - PreparedStatement preparedStatement = connection.prepareStatement("SELECT count(*) " - + "FROM information_schema.tables " - + "WHERE table_name = ?" - + "LIMIT 1;"); - preparedStatement.setString(1, tableName); - - ResultSet resultSet = preparedStatement.executeQuery(); - resultSet.next(); - return resultSet.getInt(1) != 0; - } - catch (SQLException ignored) - { - return false; - } - } - - public Connection getCon() - { - if (this.dataSource == null) - { - return null; - } - try - { - return dataSource.getConnection(); - } - catch (SQLException e) - { - e.printStackTrace(); - } - return null; + return getConnection(); } } diff --git a/server/src/main/java/dev/plex/storage/StorageType.java b/server/src/main/java/dev/plex/storage/StorageType.java index 9e5a4e1..a5e16cb 100644 --- a/server/src/main/java/dev/plex/storage/StorageType.java +++ b/server/src/main/java/dev/plex/storage/StorageType.java @@ -1,6 +1,109 @@ package dev.plex.storage; +import com.zaxxer.hikari.HikariConfig; +import dev.plex.Plex; + +import java.io.File; +import java.util.Arrays; +import java.util.Locale; +import java.util.Set; + public enum StorageType { - MARIADB, SQLITE + SQLITE("SQLite", "sqlite", "org.sqlite.JDBC", Set.of("sqlite")) + { + @Override + public void configure(HikariConfig config, Plex plugin) + { + File databaseFile = new File(plugin.getDataFolder(), "database.db"); + config.setJdbcUrl("jdbc:sqlite:" + databaseFile.getAbsolutePath()); + config.setDriverClassName(driverClass); + config.setMaximumPoolSize(1); + } + + @Override + public String migrationHistoryTableSql(String tableName) + { + return "CREATE TABLE IF NOT EXISTS " + tableName + " (version VARCHAR(100) NOT NULL PRIMARY KEY, installed_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000))"; + } + }, + + MARIADB("MariaDB", "mariadb", "org.mariadb.jdbc.Driver", Set.of("mariadb", "mysql")) + { + @Override + public void configure(HikariConfig config, Plex plugin) + { + configureRemote(config, plugin, "jdbc:mariadb://", driverClass); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + } + + @Override + public String migrationHistoryTableSql(String tableName) + { + return "CREATE TABLE IF NOT EXISTS `" + tableName + "` (`version` VARCHAR(100) NOT NULL PRIMARY KEY, `installed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"; + } + }, + + POSTGRES("PostgreSQL", "postgres", "org.postgresql.Driver", Set.of("postgres", "postgresql")) + { + @Override + public void configure(HikariConfig config, Plex plugin) + { + configureRemote(config, plugin, "jdbc:postgresql://", driverClass); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + } + }; + + private final Set aliases; + private final String displayName; + private final String migrationDirectory; + protected final String driverClass; + + StorageType(String displayName, String migrationDirectory, String driverClass, Set aliases) + { + this.aliases = aliases; + this.displayName = displayName; + this.migrationDirectory = migrationDirectory; + this.driverClass = driverClass; + } + + public static StorageType fromConfig(String value) + { + String normalized = value == null ? "sqlite" : value.trim().toLowerCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(type -> type.aliases.contains(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unsupported database storage type: " + value)); + } + + public abstract void configure(HikariConfig config, Plex plugin); + + public String migrationHistoryTableSql(String tableName) + { + return "CREATE TABLE IF NOT EXISTS " + tableName + " (version VARCHAR(100) NOT NULL PRIMARY KEY, installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"; + } + + public String getDisplayName() + { + return displayName; + } + + public String getMigrationDirectory() + { + return migrationDirectory; + } + + private static void configureRemote(HikariConfig config, Plex plugin, String jdbcPrefix, String driverClass) + { + String host = plugin.config.getString("data.db.hostname"); + int port = plugin.config.getInt("data.db.port"); + String database = plugin.config.getString("data.db.name"); + config.setJdbcUrl(jdbcPrefix + host + ":" + port + "/" + database); + config.setDriverClassName(driverClass); + config.setUsername(plugin.config.getString("data.db.user")); + config.setPassword(plugin.config.getString("data.db.password")); + } } diff --git a/server/src/main/java/dev/plex/storage/annotation/MapObjectList.java b/server/src/main/java/dev/plex/storage/annotation/MapObjectList.java deleted file mode 100644 index 154e1c8..0000000 --- a/server/src/main/java/dev/plex/storage/annotation/MapObjectList.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.plex.storage.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author Taah - * @since 1:42 AM [25-08-2023] - */ - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface MapObjectList -{ -} diff --git a/server/src/main/java/dev/plex/storage/annotation/NoLimit.java b/server/src/main/java/dev/plex/storage/annotation/NoLimit.java deleted file mode 100644 index 381837f..0000000 --- a/server/src/main/java/dev/plex/storage/annotation/NoLimit.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.plex.storage.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author Taah - * @since 1:42 AM [25-08-2023] - */ - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface NoLimit -{ -} diff --git a/server/src/main/java/dev/plex/storage/annotation/PrimaryKey.java b/server/src/main/java/dev/plex/storage/annotation/PrimaryKey.java deleted file mode 100644 index f18659c..0000000 --- a/server/src/main/java/dev/plex/storage/annotation/PrimaryKey.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.plex.storage.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author Taah - * @since 1:42 AM [25-08-2023] - */ - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface PrimaryKey -{ - boolean dontSet() default false; -} diff --git a/server/src/main/java/dev/plex/storage/annotation/SQLTable.java b/server/src/main/java/dev/plex/storage/annotation/SQLTable.java deleted file mode 100644 index 458a1dc..0000000 --- a/server/src/main/java/dev/plex/storage/annotation/SQLTable.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.plex.storage.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author Taah - * @since 4:27 AM [25-08-2023] - */ - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface SQLTable -{ - String value(); -} diff --git a/server/src/main/java/dev/plex/storage/annotation/VarcharLimit.java b/server/src/main/java/dev/plex/storage/annotation/VarcharLimit.java deleted file mode 100644 index 52117b6..0000000 --- a/server/src/main/java/dev/plex/storage/annotation/VarcharLimit.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.plex.storage.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author Taah - * @since 1:42 AM [25-08-2023] - */ - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface VarcharLimit -{ - int value(); -} diff --git a/server/src/main/java/dev/plex/storage/database/Database.java b/server/src/main/java/dev/plex/storage/database/Database.java new file mode 100644 index 0000000..43cb4fd --- /dev/null +++ b/server/src/main/java/dev/plex/storage/database/Database.java @@ -0,0 +1,173 @@ +package dev.plex.storage.database; + +import com.j256.ormlite.jdbc.DataSourceConnectionSource; +import com.j256.ormlite.support.ConnectionSource; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import dev.plex.PlexBase; +import dev.plex.storage.StorageType; +import dev.plex.util.PlexLog; +import lombok.Getter; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +@Getter +public class Database implements PlexBase +{ + private static final String MIGRATION_TABLE = "plex_schema_history"; + + private final HikariDataSource dataSource; + private final ConnectionSource connectionSource; + private final StorageType storageType; + + public Database() + { + this.storageType = StorageType.fromConfig(plugin.config.getString("data.db.storage", "sqlite")); + + HikariConfig config = new HikariConfig(); + config.setPoolName("Plex-Database"); + config.setMaxLifetime(1_800_000); + config.setIdleTimeout(600_000); + config.setKeepaliveTime(0); + config.setConnectionTimeout(60_000); + config.setMinimumIdle(2); + config.setMaximumPoolSize(10); + storageType.configure(config, plugin); + + plugin.setStorageType(this.storageType); + this.dataSource = new HikariDataSource(config); + try + { + this.connectionSource = new DataSourceConnectionSource(dataSource, config.getJdbcUrl()); + runMigrations(); + } + catch (Exception e) + { + dataSource.close(); + throw new IllegalStateException("Failed to initialize database", e); + } + } + + private void runMigrations() throws Exception + { + try (Connection connection = dataSource.getConnection()) + { + ensureMigrationTable(connection); + for (String migration : List.of("001_initial_schema")) + { + if (hasMigration(connection, migration)) + { + continue; + } + + executeMigration(connection, migration); + try (Statement statement = connection.createStatement()) + { + statement.executeUpdate("INSERT INTO " + MIGRATION_TABLE + " (version) VALUES ('" + migration + "')"); + } + PlexLog.log("Applied database migration " + migration); + } + } + } + + private void ensureMigrationTable(Connection connection) throws SQLException + { + try (Statement statement = connection.createStatement()) + { + statement.execute(storageType.migrationHistoryTableSql(MIGRATION_TABLE)); + } + } + + private boolean hasMigration(Connection connection, String migration) throws SQLException + { + try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT version FROM " + MIGRATION_TABLE + " WHERE version = '" + migration + "'")) + { + return resultSet.next(); + } + } + + private void executeMigration(Connection connection, String migration) throws Exception + { + String resource = "db/migration/" + storageType.getMigrationDirectory() + "/" + migration + ".sql"; + try (InputStream stream = getClass().getClassLoader().getResourceAsStream(resource)) + { + if (stream == null) + { + throw new IllegalStateException("Missing database migration resource: " + resource); + } + + for (String sql : splitStatements(new String(stream.readAllBytes(), StandardCharsets.UTF_8))) + { + try (Statement statement = connection.createStatement()) + { + statement.execute(sql); + } + } + } + } + + private List splitStatements(String script) + { + List statements = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + + for (int i = 0; i < script.length(); i++) + { + char c = script.charAt(i); + if (c == '\'' && !inDoubleQuote) + { + inSingleQuote = !inSingleQuote; + } + else if (c == '"' && !inSingleQuote) + { + inDoubleQuote = !inDoubleQuote; + } + + if (c == ';' && !inSingleQuote && !inDoubleQuote) + { + addStatement(statements, current); + current.setLength(0); + continue; + } + current.append(c); + } + addStatement(statements, current); + return statements; + } + + private void addStatement(List statements, StringBuilder statement) + { + String sql = statement.toString().replaceAll("(?m)^\\s*--.*$", "").trim(); + if (!sql.isEmpty()) + { + statements.add(sql); + } + } + + public Connection getConnection() throws SQLException + { + return dataSource.getConnection(); + } + + public void close() + { + try + { + connectionSource.close(); + } + catch (Exception e) + { + PlexLog.warn("Failed to close ORMLite connection source: " + e.getMessage()); + } + dataSource.close(); + } +} diff --git a/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java b/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java new file mode 100644 index 0000000..4d08d41 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java @@ -0,0 +1,34 @@ +package dev.plex.storage.database.entity; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@DatabaseTable(tableName = "notes") +public class NoteEntity +{ + @DatabaseField(generatedId = true, columnName = "row_id") + private long rowId; + + @DatabaseField(columnName = "id", index = true) + private int id; + + @DatabaseField(columnName = "uuid", canBeNull = false, index = true, width = 46) + private String uuid; + + @DatabaseField(columnName = "written_by", width = 46) + private String writtenBy; + + @DatabaseField(columnName = "note", width = 2000) + private String note; + + @DatabaseField(columnName = "timestamp") + private long timestamp; + + public NoteEntity() + { + } +} diff --git a/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java b/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java new file mode 100644 index 0000000..5a1ccd2 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java @@ -0,0 +1,43 @@ +package dev.plex.storage.database.entity; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@DatabaseTable(tableName = "players") +public class PlayerEntity +{ + @DatabaseField(id = true, columnName = "uuid", width = 46) + private String uuid; + + @DatabaseField(columnName = "name", width = 18) + private String name; + + @DatabaseField(columnName = "login_msg", width = 2000) + private String loginMessage; + + @DatabaseField(columnName = "prefix", width = 2000) + private String prefix; + + @DatabaseField(columnName = "staffChat") + private boolean staffChat; + + @DatabaseField(columnName = "ips", width = 2000) + private String ips; + + @DatabaseField(columnName = "coins") + private long coins; + + @DatabaseField(columnName = "vanished") + private boolean vanished; + + @DatabaseField(columnName = "commandspy") + private boolean commandSpy; + + public PlayerEntity() + { + } +} diff --git a/server/src/main/java/dev/plex/storage/database/entity/PlayerIpEntity.java b/server/src/main/java/dev/plex/storage/database/entity/PlayerIpEntity.java new file mode 100644 index 0000000..56cf139 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/database/entity/PlayerIpEntity.java @@ -0,0 +1,31 @@ +package dev.plex.storage.database.entity; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@DatabaseTable(tableName = "player_ips") +public class PlayerIpEntity +{ + @DatabaseField(generatedId = true, columnName = "id") + private long id; + + @DatabaseField(columnName = "player_uuid", canBeNull = false, index = true, width = 46) + private String playerUuid; + + @DatabaseField(columnName = "ip", canBeNull = false, index = true, width = 64) + private String ip; + + public PlayerIpEntity() + { + } + + public PlayerIpEntity(String playerUuid, String ip) + { + this.playerUuid = playerUuid; + this.ip = ip; + } +} diff --git a/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java b/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java new file mode 100644 index 0000000..58228c9 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java @@ -0,0 +1,52 @@ +package dev.plex.storage.database.entity; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@DatabaseTable(tableName = "punishments") +public class PunishmentEntity +{ + @DatabaseField(generatedId = true, columnName = "id") + private long id; + + @DatabaseField(columnName = "punished", canBeNull = false, index = true, width = 46) + private String punished; + + @DatabaseField(columnName = "punisher", width = 46) + private String punisher; + + @DatabaseField(columnName = "punisherName", width = 64) + private String punisherName; + + @DatabaseField(columnName = "punishedUsername", width = 16) + private String punishedUsername; + + @DatabaseField(columnName = "ip", width = 2000, index = true) + private String ip; + + @DatabaseField(columnName = "type", width = 30) + private String type; + + @DatabaseField(columnName = "reason", width = 2000) + private String reason; + + @DatabaseField(columnName = "customTime") + private boolean customTime; + + @DatabaseField(columnName = "active", index = true) + private boolean active; + + @DatabaseField(columnName = "issueDate") + private long issueDate; + + @DatabaseField(columnName = "endDate") + private long endDate; + + public PunishmentEntity() + { + } +} diff --git a/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java b/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java index 73fea83..b011282 100644 --- a/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java +++ b/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java @@ -2,72 +2,66 @@ package dev.plex.storage.player; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.stmt.DeleteBuilder; import dev.plex.Plex; import dev.plex.player.PlexPlayer; -import dev.plex.storage.StorageType; -import dev.plex.util.PlexLog; +import dev.plex.storage.database.entity.PlayerEntity; +import dev.plex.storage.database.entity.PlayerIpEntity; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.UUID; /** - * SQL fetching utilities for players + * Player persistence backed by ORMLite. */ public class SQLPlayerData { - private final String SELECT = "SELECT * FROM `players` WHERE uuid=?"; - private final String UPDATE = "UPDATE `players` SET name=?, login_msg=?, prefix=?, ips=?, coins=?, vanished=?, commandspy=? WHERE uuid=?"; - private final String INSERT = "INSERT INTO `players` (`uuid`, `name`, `login_msg`, `prefix`, `ips`, `coins`, `vanished`, `commandspy`) VALUES (?, ?, ?, ?, ?, ?, ?, ?);"; + private static final Gson GSON = new Gson(); + private final Dao players; + private final Dao playerIps; + + public SQLPlayerData() + { + try + { + this.players = DaoManager.createDao(Plex.get().getSqlConnection().getConnectionSource(), PlayerEntity.class); + this.playerIps = DaoManager.createDao(Plex.get().getSqlConnection().getConnectionSource(), PlayerIpEntity.class); + } + catch (SQLException e) + { + throw new IllegalStateException("Failed to create player DAOs", e); + } + } - /** - * Checks if a player exists in the SQL database - * - * @param uuid The unique ID of the player - * @return true if the player was found in the database - */ public boolean exists(UUID uuid) { - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(SELECT); - statement.setString(1, uuid.toString()); - ResultSet set = statement.executeQuery(); - return set.next(); + return players.idExists(uuid.toString()); } - catch (SQLException throwables) + catch (SQLException e) { - throwables.printStackTrace(); + e.printStackTrace(); + return false; } - return false; } public boolean exists(String username) { - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement("SELECT * FROM `players` WHERE name=?"); - statement.setString(1, username); - ResultSet set = statement.executeQuery(); - return set.next(); + return players.queryBuilder().where().eq("name", username).queryForFirst() != null; } - catch (SQLException throwables) + catch (SQLException e) { - throwables.printStackTrace(); + e.printStackTrace(); + return false; } - return false; } - /** - * Gets the player from cache or from the SQL database - * - * @param uuid The unique ID of the player - * @return a PlexPlayer object - * @see PlexPlayer - */ public PlexPlayer getByUUID(UUID uuid, boolean loadExtraData) { if (Plex.get().getPlayerCache().getPlexPlayerMap().containsKey(uuid)) @@ -75,48 +69,17 @@ public class SQLPlayerData return Plex.get().getPlayerCache().getPlexPlayerMap().get(uuid); } - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(SELECT); - statement.setString(1, uuid.toString()); - ResultSet set = statement.executeQuery(); - PlexPlayer plexPlayer = new PlexPlayer(uuid, loadExtraData); - while (set.next()) - { - String name = set.getString("name"); - String loginMSG = set.getString("login_msg"); - String prefix = set.getString("prefix"); - long coins = set.getLong("coins"); - boolean vanished = set.getBoolean("vanished"); - boolean commandspy = set.getBoolean("commandspy"); - List ips = new Gson().fromJson(set.getString("ips"), new TypeToken>() - { - }.getType()); - plexPlayer.setName(name); - plexPlayer.setLoginMessage(loginMSG); - plexPlayer.setPrefix(prefix); - plexPlayer.setIps(ips); - plexPlayer.setCoins(coins); - plexPlayer.setVanished(vanished); - plexPlayer.setCommandSpy(commandspy); - } - return plexPlayer; + return toPlayer(players.queryForId(uuid.toString()), loadExtraData); } - catch (SQLException throwables) + catch (SQLException e) { - throwables.printStackTrace(); + e.printStackTrace(); + return null; } - return null; } - - /** - * Gets the player from cache or from the SQL database - * - * @param uuid The unique ID of the player - * @return a PlexPlayer object - * @see PlexPlayer - */ public String getNameByUUID(UUID uuid) { if (Plex.get().getPlayerCache().getPlexPlayerMap().containsKey(uuid)) @@ -124,67 +87,43 @@ public class SQLPlayerData return Plex.get().getPlayerCache().getPlexPlayerMap().get(uuid).getName(); } - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement("SELECT `name` FROM `players` WHERE uuid=?"); - statement.setString(1, uuid.toString()); - ResultSet set = statement.executeQuery(); - if (set.next()) - { - return set.getString("name"); - } + PlayerEntity entity = players.queryForId(uuid.toString()); + return entity == null ? null : entity.getName(); } - catch (SQLException throwables) + catch (SQLException e) { - throwables.printStackTrace(); + e.printStackTrace(); + return null; } - return null; } public PlexPlayer getByUUID(UUID uuid) { - return this.getByUUID(uuid, true); + return getByUUID(uuid, true); } public PlexPlayer getByName(String username, boolean loadExtraData) { - PlexPlayer player = Plex.get().getPlayerCache().getPlexPlayerMap().values().stream().filter(plexPlayer -> plexPlayer.getName().equalsIgnoreCase(username)).findFirst().orElse(null); + PlexPlayer player = Plex.get().getPlayerCache().getPlexPlayerMap().values().stream() + .filter(plexPlayer -> plexPlayer.getName().equalsIgnoreCase(username)) + .findFirst() + .orElse(null); if (player != null) { return player; } - try (Connection con = Plex.get().getSqlConnection().getCon()) + + try { - PreparedStatement statement = con.prepareStatement("SELECT * FROM `players` WHERE name=? LIMIT 1"); - statement.setString(1, username); - ResultSet set = statement.executeQuery(); - while (set.next()) - { - PlexPlayer plexPlayer = new PlexPlayer(UUID.fromString(set.getString("uuid")), loadExtraData); - String loginMSG = set.getString("login_msg"); - String prefix = set.getString("prefix"); - long coins = set.getLong("coins"); - boolean vanished = set.getBoolean("vanished"); - boolean commandspy = set.getBoolean("commandspy"); - List ips = new Gson().fromJson(set.getString("ips"), new TypeToken>() - { - }.getType()); - plexPlayer.setName(username); - plexPlayer.setLoginMessage(loginMSG); - plexPlayer.setPrefix(prefix); - plexPlayer.setIps(ips); - plexPlayer.setCoins(coins); - plexPlayer.setVanished(vanished); - plexPlayer.setCommandSpy(commandspy); - return plexPlayer; - } + return toPlayer(players.queryBuilder().limit(1L).where().eq("name", username).queryForFirst(), loadExtraData); + } + catch (SQLException e) + { + e.printStackTrace(); return null; } - catch (SQLException throwables) - { - throwables.printStackTrace(); - } - return null; } public PlexPlayer getByName(String username) @@ -192,155 +131,115 @@ public class SQLPlayerData return getByName(username, true); } - /** - * Gets the player from cache or from the SQL database - * - * @param ip The IP address of the player. - * @return a PlexPlayer object - * @see PlexPlayer - */ public PlexPlayer getByIP(String ip) { - PlexPlayer player = Plex.get().getPlayerCache().getPlexPlayerMap().values().stream().filter(plexPlayer -> plexPlayer.getIps().contains(ip)).findFirst().orElse(null); + PlexPlayer player = Plex.get().getPlayerCache().getPlexPlayerMap().values().stream() + .filter(plexPlayer -> plexPlayer.getIps().contains(ip)) + .findFirst() + .orElse(null); if (player != null) { return player; } - if (Plex.get().getStorageType() == StorageType.MARIADB) + try { - try (Connection con = Plex.get().getSqlConnection().getCon()) + PlayerIpEntity playerIp = playerIps.queryBuilder().limit(1L).where().eq("ip", ip).queryForFirst(); + if (playerIp != null) { - PreparedStatement statement = con.prepareStatement("select * from `players` where json_search(ips, ?, ?) IS NOT NULL LIMIT 1"); - statement.setString(1, "one"); - statement.setString(2, ip); - ResultSet set = statement.executeQuery(); - - PlexPlayer plexPlayer = null; - while (set.next()) - { - String uuid = set.getString("uuid"); - String name = set.getString("name"); - String loginMSG = set.getString("login_msg"); - String prefix = set.getString("prefix"); - long coins = set.getLong("coins"); - boolean vanished = set.getBoolean("vanished"); - boolean commandspy = set.getBoolean("commandspy"); - List ips = new Gson().fromJson(set.getString("ips"), new TypeToken>() - { - }.getType()); - plexPlayer = new PlexPlayer(UUID.fromString(uuid)); - plexPlayer.setName(name); - plexPlayer.setLoginMessage(loginMSG); - plexPlayer.setPrefix(prefix); - plexPlayer.setIps(ips); - plexPlayer.setCoins(coins); - plexPlayer.setVanished(vanished); - plexPlayer.setCommandSpy(commandspy); - } - return plexPlayer; + return toPlayer(players.queryForId(playerIp.getPlayerUuid()), true); } - catch (SQLException throwables) + + for (PlayerEntity entity : players.queryForAll()) { - throwables.printStackTrace(); + List ips = parseIps(entity.getIps()); + if (ips.contains(ip)) + { + syncIps(entity.getUuid(), ips); + return toPlayer(entity, true); + } } } - else if (Plex.get().getStorageType() == StorageType.SQLITE) + catch (SQLException e) { - PlexLog.warn("Querying a user by IP running SQLite can cause performance issues! Please try to switch to a remote DB ASAP!"); - try (Connection con = Plex.get().getSqlConnection().getCon()) - { - PreparedStatement statement = con.prepareStatement("select * from `players`"); - ResultSet set = statement.executeQuery(); - - PlexPlayer plexPlayer = null; - while (set.next()) - { - List ips = new Gson().fromJson(set.getString("ips"), new TypeToken>() - { - }.getType()); - if (!ips.contains(ip)) - { - continue; - } - String uuid = set.getString("uuid"); - String name = set.getString("name"); - String loginMSG = set.getString("login_msg"); - String prefix = set.getString("prefix"); - long coins = set.getLong("coins"); - boolean vanished = set.getBoolean("vanished"); - boolean commandspy = set.getBoolean("commandspy"); - - plexPlayer = new PlexPlayer(UUID.fromString(uuid)); - plexPlayer.setName(name); - plexPlayer.setLoginMessage(loginMSG); - plexPlayer.setPrefix(prefix); - plexPlayer.setIps(ips); - plexPlayer.setCoins(coins); - plexPlayer.setVanished(vanished); - plexPlayer.setCommandSpy(commandspy); - } - return plexPlayer; - } - catch (SQLException throwables) - { - throwables.printStackTrace(); - } + e.printStackTrace(); } return null; } - /** - * Updates a player's information in the SQL database - * - * @param player The PlexPlayer object - * @see PlexPlayer - */ public void update(PlexPlayer player) { - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(UPDATE); - statement.setString(1, player.getName()); - statement.setString(2, player.getLoginMessage()); - statement.setString(3, player.getPrefix()); - statement.setString(4, new Gson().toJson(player.getIps())); - statement.setLong(5, player.getCoins()); - statement.setBoolean(6, player.isVanished()); - statement.setBoolean(7, player.isCommandSpy()); - statement.setString(8, player.getUuid().toString()); - statement.executeUpdate(); + players.createOrUpdate(toEntity(player)); + syncIps(player.getUuid().toString(), player.getIps()); } - catch (SQLException throwables) + catch (SQLException e) { - throwables.printStackTrace(); + e.printStackTrace(); } } - /** - * Inserts the player's information in the database - * - * @param player The PlexPlayer object - * @see PlexPlayer - */ public void insert(PlexPlayer player) { - try (Connection con = Plex.get().getSqlConnection().getCon()) + update(player); + } + + private PlexPlayer toPlayer(PlayerEntity entity, boolean loadExtraData) + { + if (entity == null) { - PreparedStatement statement = con.prepareStatement(INSERT); - statement.setString(1, player.getUuid().toString()); - statement.setString(2, player.getName()); - statement.setString(3, player.getLoginMessage()); - statement.setString(4, player.getPrefix()); - statement.setString(5, new Gson().toJson(player.getIps())); - statement.setLong(6, player.getCoins()); - statement.setBoolean(7, player.isVanished()); - statement.setBoolean(8, player.isCommandSpy()); - statement.execute(); + return null; } - catch (SQLException throwables) + + PlexPlayer plexPlayer = new PlexPlayer(UUID.fromString(entity.getUuid()), loadExtraData); + plexPlayer.setName(entity.getName()); + plexPlayer.setLoginMessage(entity.getLoginMessage()); + plexPlayer.setPrefix(entity.getPrefix()); + plexPlayer.setStaffChat(entity.isStaffChat()); + plexPlayer.setIps(parseIps(entity.getIps())); + plexPlayer.setCoins(entity.getCoins()); + plexPlayer.setVanished(entity.isVanished()); + plexPlayer.setCommandSpy(entity.isCommandSpy()); + return plexPlayer; + } + + private PlayerEntity toEntity(PlexPlayer player) + { + PlayerEntity entity = new PlayerEntity(); + entity.setUuid(player.getUuid().toString()); + entity.setName(player.getName()); + entity.setLoginMessage(player.getLoginMessage()); + entity.setPrefix(player.getPrefix()); + entity.setStaffChat(player.isStaffChat()); + entity.setIps(GSON.toJson(player.getIps())); + entity.setCoins(player.getCoins()); + entity.setVanished(player.isVanished()); + entity.setCommandSpy(player.isCommandSpy()); + return entity; + } + + private List parseIps(String ips) + { + if (ips == null || ips.isBlank()) { - throwables.printStackTrace(); + return List.of(); + } + List parsed = GSON.fromJson(ips, new TypeToken>() + { + }.getType()); + return parsed == null ? List.of() : parsed; + } + + private void syncIps(String playerUuid, List ips) throws SQLException + { + DeleteBuilder delete = playerIps.deleteBuilder(); + delete.where().eq("player_uuid", playerUuid); + delete.delete(); + + for (String ip : ips.stream().distinct().toList()) + { + playerIps.create(new PlayerIpEntity(playerUuid, ip)); } } } diff --git a/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java b/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java index eb512dc..55333fe 100644 --- a/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java +++ b/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java @@ -1,55 +1,55 @@ package dev.plex.storage.punishment; import com.google.common.collect.Lists; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.stmt.DeleteBuilder; import dev.plex.Plex; import dev.plex.punishment.extra.Note; +import dev.plex.storage.database.entity.NoteEntity; import dev.plex.util.TimeUtils; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Comparator; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class SQLNotes { - private static final String SELECT = "SELECT * FROM `notes` WHERE uuid=?"; - private static final String INSERT = "INSERT INTO `notes` (`id`, `uuid`, `written_by`, `note`, `timestamp`) VALUES(?, ?, ?, ?, ?)"; - private static final String DELETE = "DELETE FROM `notes` WHERE uuid=? AND id=?"; + private final Dao notes; + + public SQLNotes() + { + try + { + this.notes = DaoManager.createDao(Plex.get().getSqlConnection().getConnectionSource(), NoteEntity.class); + } + catch (SQLException e) + { + throw new IllegalStateException("Failed to create note DAO", e); + } + } public CompletableFuture> getNotes(UUID uuid) { return CompletableFuture.supplyAsync(() -> { - List notes = Lists.newArrayList(); - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(SELECT); - statement.setString(1, uuid.toString()); - ResultSet set = statement.executeQuery(); - while (set.next()) - { - Note note = new Note( - uuid, - set.getString("note"), - UUID.fromString(set.getString("written_by")), - ZonedDateTime.ofInstant(Instant.ofEpochMilli(set.getLong("timestamp")), ZoneId.of(TimeUtils.TIMEZONE)) - ); - note.setId(set.getInt("id")); - notes.add(note); - } + return notes.queryForEq("uuid", uuid.toString()).stream() + .sorted(Comparator.comparingInt(NoteEntity::getId)) + .map(this::toNote) + .toList(); } catch (SQLException e) { e.printStackTrace(); - return notes; + return Lists.newArrayList(); } - return notes; }); } @@ -57,12 +57,11 @@ public class SQLNotes { return CompletableFuture.runAsync(() -> { - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(DELETE); - statement.setString(1, uuid.toString()); - statement.setInt(2, id); - statement.execute(); + DeleteBuilder delete = notes.deleteBuilder(); + delete.where().eq("uuid", uuid.toString()).and().eq("id", id); + delete.delete(); } catch (SQLException e) { @@ -75,24 +74,44 @@ public class SQLNotes { return CompletableFuture.runAsync(() -> { - getNotes(note.getUuid()).whenComplete((notes, throwable) -> + try { - try (Connection con = Plex.get().getSqlConnection().getCon()) - { - PreparedStatement statement = con.prepareStatement(INSERT); - statement.setInt(1, notes.size() + 1); - statement.setString(2, note.getUuid().toString()); - statement.setString(3, note.getWrittenBy().toString()); - statement.setString(4, note.getNote()); - statement.setLong(5, note.getTimestamp().toInstant().toEpochMilli()); - statement.execute(); - note.setId(notes.size()); - } - catch (SQLException e) - { - e.printStackTrace(); - } - }); + int nextId = notes.queryForEq("uuid", note.getUuid().toString()).stream() + .map(NoteEntity::getId) + .max(Integer::compareTo) + .orElse(0) + 1; + NoteEntity entity = toEntity(note); + entity.setId(nextId); + notes.create(entity); + note.setId(nextId); + } + catch (SQLException e) + { + e.printStackTrace(); + } }); } + + private Note toNote(NoteEntity entity) + { + Note note = new Note( + UUID.fromString(entity.getUuid()), + entity.getNote(), + UUID.fromString(entity.getWrittenBy()), + ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getTimestamp()), ZoneId.of(TimeUtils.TIMEZONE)) + ); + note.setId(entity.getId()); + return note; + } + + private NoteEntity toEntity(Note note) + { + NoteEntity entity = new NoteEntity(); + entity.setId(note.getId()); + entity.setUuid(note.getUuid().toString()); + entity.setWrittenBy(note.getWrittenBy().toString()); + entity.setNote(note.getNote()); + entity.setTimestamp(note.getTimestamp().toInstant().toEpochMilli()); + return entity; + } } diff --git a/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java b/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java index 826fe88..088c65c 100644 --- a/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java +++ b/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java @@ -1,15 +1,16 @@ package dev.plex.storage.punishment; import com.google.common.collect.Lists; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.stmt.UpdateBuilder; import dev.plex.Plex; import dev.plex.punishment.Punishment; import dev.plex.punishment.PunishmentType; +import dev.plex.storage.database.entity.PunishmentEntity; import dev.plex.util.PlexLog; import dev.plex.util.TimeUtils; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; import java.time.ZoneId; @@ -20,122 +21,70 @@ import java.util.concurrent.CompletableFuture; public class SQLPunishment { - private static final String SELECT = "SELECT * FROM `punishments` WHERE punished=?"; - private static final String SELECT_BY_IP = "SELECT * FROM `punishments` WHERE ip=?"; - private static final String SELECT_BY = "SELECT * FROM `punishments` WHERE punisher=?"; + private final Dao punishments; - private static final String INSERT = "INSERT INTO `punishments` (`punished`, `punisher`, `punisherName`, `punishedUsername`, `ip`, `type`, `reason`, `customTime`, `active`, `endDate`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - private static final String UPDATE_PUNISHMENT = "UPDATE `punishments` SET active=? WHERE punished=? AND type=?"; + public SQLPunishment() + { + try + { + this.punishments = DaoManager.createDao(Plex.get().getSqlConnection().getConnectionSource(), PunishmentEntity.class); + } + catch (SQLException e) + { + throw new IllegalStateException("Failed to create punishment DAO", e); + } + } public CompletableFuture> getPunishments() { return CompletableFuture.supplyAsync(() -> { - List punishments = Lists.newArrayList(); - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement("SELECT * FROM `punishments`"); - ResultSet set = statement.executeQuery(); - while (set.next()) - { - Punishment punishment = new Punishment(UUID.fromString(set.getString("punished")), set.getString("punisher") != null && set.getString("punisher").isEmpty() ? UUID.fromString(set.getString("punisher")) : null); - punishment.setActive(set.getBoolean("active")); - punishment.setType(PunishmentType.valueOf(set.getString("type"))); - punishment.setCustomTime(set.getBoolean("customTime")); - punishment.setPunishedUsername(set.getString("punishedUsername")); - punishment.setPunisherName(set.getString("punisherName")); - punishment.setEndDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(set.getLong("endDate")), ZoneId.of(TimeUtils.TIMEZONE))); - punishment.setReason(set.getString("reason")); - punishment.setIp(set.getString("ip")); - punishments.add(punishment); - } + return punishments.queryForAll().stream().map(this::toPunishment).toList(); } catch (SQLException e) { e.printStackTrace(); - return punishments; + return Lists.newArrayList(); } - return punishments; }); } public List getPunishments(UUID uuid) { - List punishments = Lists.newArrayList(); - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(SELECT); - statement.setString(1, uuid.toString()); - ResultSet set = statement.executeQuery(); - while (set.next()) - { - Punishment punishment = new Punishment(UUID.fromString(set.getString("punished")), set.getString("punisher") == null ? null : UUID.fromString(set.getString("punisher"))); - punishment.setActive(set.getBoolean("active")); - punishment.setType(PunishmentType.valueOf(set.getString("type"))); - punishment.setCustomTime(set.getBoolean("customTime")); - punishment.setPunishedUsername(set.getString("punishedUsername")); - punishment.setEndDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(set.getLong("endDate")), ZoneId.of(TimeUtils.TIMEZONE))); - punishment.setReason(set.getString("reason")); - punishment.setIp(set.getString("ip")); - punishments.add(punishment); - } + return punishments.queryForEq("punished", uuid.toString()).stream().map(this::toPunishment).toList(); } catch (SQLException e) { e.printStackTrace(); + return Lists.newArrayList(); } - return punishments; } public List getPunishments(String ip) { - List punishments = Lists.newArrayList(); - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PreparedStatement statement = con.prepareStatement(SELECT_BY_IP); - statement.setString(1, ip); - ResultSet set = statement.executeQuery(); - while (set.next()) - { - Punishment punishment = new Punishment(UUID.fromString(set.getString("punished")), set.getString("punisher") == null ? null : UUID.fromString(set.getString("punisher"))); - punishment.setActive(set.getBoolean("active")); - punishment.setType(PunishmentType.valueOf(set.getString("type"))); - punishment.setCustomTime(set.getBoolean("customTime")); - punishment.setPunishedUsername(set.getString("punishedUsername")); - punishment.setEndDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(set.getLong("endDate")), ZoneId.of(TimeUtils.TIMEZONE))); - punishment.setReason(set.getString("reason")); - punishment.setIp(set.getString("ip")); - punishments.add(punishment); - } + return punishments.queryForEq("ip", ip).stream().map(this::toPunishment).toList(); } catch (SQLException e) { e.printStackTrace(); + return Lists.newArrayList(); } - return punishments; } public CompletableFuture insertPunishment(Punishment punishment) { - return CompletableFuture.runAsync(() -> { - try (Connection con = Plex.get().getSqlConnection().getCon()) + try { - PlexLog.debug("Running execute punishment on " + punishment.getPunished().toString()); - PreparedStatement statement = con.prepareStatement(INSERT); - statement.setString(1, punishment.getPunished().toString()); - statement.setString(2, punishment.getPunisher() == null ? null : punishment.getPunisher().toString()); - statement.setString(3, punishment.getPunisherName()); - statement.setString(4, punishment.getPunishedUsername()); - statement.setString(5, punishment.getIp()); - statement.setString(6, punishment.getType().name()); - statement.setString(7, punishment.getReason()); - statement.setBoolean(8, punishment.isCustomTime()); - statement.setBoolean(9, punishment.isActive()); - statement.setLong(10, punishment.getEndDate().toInstant().toEpochMilli()); - PlexLog.debug("Executing punishment"); - statement.execute(); + PlexLog.debug("Persisting punishment for " + punishment.getPunished()); + punishments.create(toEntity(punishment)); } catch (SQLException e) { @@ -146,19 +95,28 @@ public class SQLPunishment public void syncRemoveBan(UUID uuid) { - try (Connection con = Plex.get().getSqlConnection().getCon()) - { - PreparedStatement statement = con.prepareStatement(UPDATE_PUNISHMENT); - statement.setBoolean(1, false); - statement.setString(2, uuid.toString()); - statement.setString(3, PunishmentType.BAN.name()); - statement.executeUpdate(); + setActive(uuid, PunishmentType.BAN, false); + setActive(uuid, PunishmentType.TEMPBAN, false); + } - PreparedStatement statement1 = con.prepareStatement(UPDATE_PUNISHMENT); - statement1.setBoolean(1, false); - statement1.setString(2, uuid.toString()); - statement1.setString(3, PunishmentType.TEMPBAN.name()); - statement1.executeUpdate(); + public CompletableFuture updatePunishment(PunishmentType type, boolean active, UUID punished) + { + return CompletableFuture.runAsync(() -> setActive(punished, type, active)); + } + + public CompletableFuture removeBan(UUID uuid) + { + return CompletableFuture.runAsync(() -> syncRemoveBan(uuid)); + } + + private void setActive(UUID punished, PunishmentType type, boolean active) + { + try + { + UpdateBuilder update = punishments.updateBuilder(); + update.updateColumnValue("active", active); + update.where().eq("punished", punished.toString()).and().eq("type", type.name()); + update.update(); } catch (SQLException e) { @@ -166,48 +124,36 @@ public class SQLPunishment } } - public CompletableFuture updatePunishment(PunishmentType type, boolean active, UUID punished) + private Punishment toPunishment(PunishmentEntity entity) { - return CompletableFuture.runAsync(() -> - { - try (Connection con = Plex.get().getSqlConnection().getCon()) - { - PreparedStatement statement = con.prepareStatement(UPDATE_PUNISHMENT); - statement.setBoolean(1, active); - statement.setString(2, punished.toString()); - statement.setString(3, type.name()); - statement.executeUpdate(); - } - catch (SQLException e) - { - e.printStackTrace(); - } - }); + UUID punisher = entity.getPunisher() == null || entity.getPunisher().isBlank() ? null : UUID.fromString(entity.getPunisher()); + Punishment punishment = new Punishment(UUID.fromString(entity.getPunished()), punisher); + punishment.setActive(entity.isActive()); + punishment.setType(PunishmentType.valueOf(entity.getType())); + punishment.setCustomTime(entity.isCustomTime()); + punishment.setPunishedUsername(entity.getPunishedUsername()); + punishment.setPunisherName(entity.getPunisherName()); + punishment.setIssueDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getIssueDate()), ZoneId.of(TimeUtils.TIMEZONE))); + punishment.setEndDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndDate()), ZoneId.of(TimeUtils.TIMEZONE))); + punishment.setReason(entity.getReason()); + punishment.setIp(entity.getIp()); + return punishment; } - public CompletableFuture removeBan(UUID uuid) + private PunishmentEntity toEntity(Punishment punishment) { - return CompletableFuture.runAsync(() -> - { - try (Connection con = Plex.get().getSqlConnection().getCon()) - { - PreparedStatement statement = con.prepareStatement(UPDATE_PUNISHMENT); - statement.setBoolean(1, false); - statement.setString(2, uuid.toString()); - statement.setString(3, PunishmentType.BAN.name()); - statement.executeUpdate(); - - PreparedStatement statement1 = con.prepareStatement(UPDATE_PUNISHMENT); - statement1.setBoolean(1, false); - statement1.setString(2, uuid.toString()); - statement1.setString(3, PunishmentType.TEMPBAN.name()); - statement1.executeUpdate(); - } - catch (SQLException e) - { - e.printStackTrace(); - } - }); + PunishmentEntity entity = new PunishmentEntity(); + entity.setPunished(punishment.getPunished().toString()); + entity.setPunisher(punishment.getPunisher() == null ? null : punishment.getPunisher().toString()); + entity.setPunisherName(punishment.getPunisherName()); + entity.setPunishedUsername(punishment.getPunishedUsername()); + entity.setIp(punishment.getIp()); + entity.setType(punishment.getType().name()); + entity.setReason(punishment.getReason()); + entity.setCustomTime(punishment.isCustomTime()); + entity.setActive(punishment.isActive()); + entity.setIssueDate(punishment.getIssueDate().toInstant().toEpochMilli()); + entity.setEndDate(punishment.getEndDate().toInstant().toEpochMilli()); + return entity; } - } diff --git a/server/src/main/java/dev/plex/util/PlexUtils.java b/server/src/main/java/dev/plex/util/PlexUtils.java index 57cf507..2bd5ccf 100644 --- a/server/src/main/java/dev/plex/util/PlexUtils.java +++ b/server/src/main/java/dev/plex/util/PlexUtils.java @@ -5,7 +5,6 @@ import com.google.common.collect.Lists; import dev.plex.Plex; import dev.plex.PlexBase; import dev.plex.listener.impl.ChatListener; -import dev.plex.storage.StorageType; import dev.plex.util.minimessage.SafeMiniMessage; import java.sql.Connection; @@ -85,14 +84,7 @@ public class PlexUtils implements PlexBase { try (Connection ignored = Plex.get().getSqlConnection().getCon()) { - if (Plex.get().getStorageType() == StorageType.MARIADB) - { - PlexLog.log("Successfully enabled MySQL!"); - } - else if (Plex.get().getStorageType() == StorageType.SQLITE) - { - PlexLog.log("Successfully enabled SQLite!"); - } + PlexLog.log("Successfully enabled " + Plex.get().getStorageType().getDisplayName() + "!"); } catch (SQLException e) { diff --git a/server/src/main/java/dev/plex/util/sql/SQLUtil.java b/server/src/main/java/dev/plex/util/sql/SQLUtil.java deleted file mode 100644 index ba4df1a..0000000 --- a/server/src/main/java/dev/plex/util/sql/SQLUtil.java +++ /dev/null @@ -1,204 +0,0 @@ -package dev.plex.util.sql; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import dev.plex.punishment.PunishmentType; -import dev.plex.storage.annotation.MapObjectList; -import dev.plex.storage.annotation.NoLimit; -import dev.plex.storage.annotation.PrimaryKey; -import dev.plex.storage.annotation.SQLTable; -import dev.plex.storage.annotation.VarcharLimit; -import dev.plex.util.PlexLog; -import dev.plex.util.ReflectionsUtil; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Collectors; - -import lombok.experimental.Accessors; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; - -/** - * @author Taah - * @since 4:28 AM [25-08-2023] - */ -public class SQLUtil -{ - public static final Map TABLES = Maps.newHashMap(); - - public static List createTable(List result, Class clazz) - { - if (!clazz.isAnnotationPresent(SQLTable.class)) - { - PlexLog.error("Unable to map {0} to a table, it is missing the SQLTable's annotation", clazz.getName()); - return null; - } - final List collectionFields = Lists.newArrayList(); - - final Table table = new Table(clazz.getAnnotation(SQLTable.class).value()); - - final StringBuilder mainResult = new StringBuilder("CREATE TABLE IF NOT EXISTS `" + table.name() + "` ("); - final List declaredFields = Arrays.stream(clazz.getDeclaredFields()).filter(field -> !Modifier.isTransient(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())).collect(Collectors.toList()); - final List iterating = declaredFields.stream().toList(); - for (Field value : iterating) - { - if (Collection.class.isAssignableFrom(value.getType())) - { - collectionFields.add(value); - declaredFields.remove(value); - } - } - Field primaryKey = null; - - for (int i = 0; i < declaredFields.size(); i++) - { - Field declaredField = declaredFields.get(i); - final Mapper mapped = Mapper.getByClass(declaredField.getType()); - if (mapped == null) - { - PlexLog.warn("Could not map field {0} for class {1}", declaredField.getName(), clazz.getName()); - continue; - } - if (declaredField.isAnnotationPresent(PrimaryKey.class)) - { - if (primaryKey != null) - { - PlexLog.error("You can only have one primary key for a table! The class {0} has more than one!", clazz.getName()); - return ImmutableList.of(); - } - primaryKey = declaredField; - } - - writeFieldToSQL(table, mainResult, mapped, declaredField); - if (i < declaredFields.size() - 1) - { - mainResult.append(", "); - } - } - if (primaryKey != null && !primaryKey.getAnnotation(PrimaryKey.class).dontSet()) - { - mainResult.append(", PRIMARY KEY (`").append(primaryKey.getName()).append("`)"); - } - mainResult.append(");"); - result.add(mainResult.toString()); - - TABLES.put(table.name(), table); - - if (primaryKey == null && !collectionFields.isEmpty()) - { - PlexLog.error("You must define a primary key to point to if you wish to have a list saved. You can use @PrimaryKey(dontSet = true) to make sure that SQL does not save it as a primary key."); - return ImmutableList.of(); - } - - Field finalPrimaryKey = primaryKey; - collectionFields.forEach(field -> - { - final String tableName = field.getName() + "To" + StringUtils.capitalize(clazz.getSimpleName()); - StringBuilder sql = new StringBuilder("CREATE TABLE IF NOT EXISTS `" + tableName + "` ("); - if (field.isAnnotationPresent(MapObjectList.class)) - { - createTable(result, ReflectionsUtil.getGenericField(field)); - return; - } - final Mapper mapped = Mapper.getByClass(ReflectionsUtil.getGenericField(field)); - if (mapped == null) - { - PlexLog.warn("Could not map collection field {0} for class {1}", field.getName(), clazz.getName()); - return; - } - final Table listTable = new Table(tableName); - writeFieldToSQL(listTable, sql, mapped, field); - sql.append(", "); - writeFieldToSQL(listTable, sql, Mapper.getByClass(finalPrimaryKey.getType()), finalPrimaryKey); - sql.append(");"); - result.add(sql.toString()); - table.mappedTables().put(field, listTable); - }); - return result; - } - - public static void update(String tableName, Object object) - { - final Table table = TABLES.get(tableName); - if (table == null) - { - PlexLog.error("Table {0} was not found", tableName); - return; - } - - } - - private static void writeFieldToSQL(Table table, StringBuilder sb, Mapper mapped, Field field) - { - - sb.append("`").append(field.getName()).append("` "); - if (mapped == Mapper.VARCHAR) - { - if (field.isAnnotationPresent(NoLimit.class)) - { - sb.append("TEXT"); - table.columns().put(field.getName(), Mapper.TEXT); - } - else - { - sb.append(mapped.name()); - table.columns().put(field.getName(), mapped); - } - } - else - { - sb.append(mapped.name()); - table.columns().put(field.getName(), mapped); - } - if (mapped == Mapper.VARCHAR && !field.isAnnotationPresent(NoLimit.class)) - { - if (UUID.class.isAssignableFrom(field.getType())) - { - sb.append(" (").append(36).append(")"); - } - else if (field.isAnnotationPresent(VarcharLimit.class)) - { - int limit = field.getAnnotation(VarcharLimit.class).value(); - sb.append(" (").append(limit).append(")"); - } - else - { - sb.append("(65535)"); - } - } - if (field.isAnnotationPresent(NotNull.class)) - { - sb.append(" NOT NULL"); - } - } - - @Accessors(fluent = true) - public enum Mapper - { - VARCHAR(String.class, UUID.class, PunishmentType.class), - BOOLEAN(Boolean.class, boolean.class), - BIGINT(Long.class, long.class, ZonedDateTime.class), - INT(Integer.class, int.class), - TEXT; - - private final Class[] clazz; - - Mapper(Class... clazz) - { - this.clazz = clazz; - } - - public static Mapper getByClass(Class clazz) - { - return Arrays.stream(values()).filter(mapper -> mapper.clazz != null && Arrays.asList(mapper.clazz).contains(clazz)).findFirst().orElse(null); - } - } -} diff --git a/server/src/main/java/dev/plex/util/sql/Table.java b/server/src/main/java/dev/plex/util/sql/Table.java deleted file mode 100644 index a9bfaea..0000000 --- a/server/src/main/java/dev/plex/util/sql/Table.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.plex.util.sql; - -import com.google.common.collect.Maps; - -import java.lang.reflect.Field; -import java.util.Map; - -import lombok.Data; -import lombok.experimental.Accessors; - -/** - * @author Taah - * @since 5:30 AM [26-08-2023] - */ - -@Data -@Accessors(fluent = true) -public class Table -{ - private final String name; - private final Map columns = Maps.newHashMap(); - private final Map mappedTables = Maps.newHashMap(); -} diff --git a/server/src/main/resources/config.yml b/server/src/main/resources/config.yml index 5a6dd8c..1cedc16 100644 --- a/server/src/main/resources/config.yml +++ b/server/src/main/resources/config.yml @@ -41,14 +41,14 @@ loginmessages: name: true data: - central: - storage: sqlite # Use mariadb, or sqlite here + db: + storage: sqlite # Use mariadb, postgres, or sqlite here user: "" password: "" hostname: 127.0.0.1 - port: 27017 - db: "plex" - side: # This is Redis, leave password blank if auth is false + port: 3306 + name: "plex" + redis: # Leave password blank if auth is false enabled: false auth: true hostname: 127.0.0.1 diff --git a/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql b/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql new file mode 100644 index 0000000..a945040 --- /dev/null +++ b/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS `players` ( + `uuid` VARCHAR(46) NOT NULL, + `name` VARCHAR(18), + `login_msg` VARCHAR(2000), + `prefix` VARCHAR(2000), + `staffChat` BOOLEAN, + `ips` VARCHAR(2000), + `coins` BIGINT, + `vanished` BOOLEAN, + `commandspy` BOOLEAN, + PRIMARY KEY (`uuid`), + INDEX `idx_players_name` (`name`) +); + +CREATE TABLE IF NOT EXISTS `punishments` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `punished` VARCHAR(46) NOT NULL, + `punisher` VARCHAR(46), + `punisherName` VARCHAR(64), + `punishedUsername` VARCHAR(16), + `ip` VARCHAR(2000), + `type` VARCHAR(30), + `reason` VARCHAR(2000), + `customTime` BOOLEAN, + `active` BOOLEAN, + `issueDate` BIGINT NOT NULL, + `endDate` BIGINT, + PRIMARY KEY (`id`), + INDEX `idx_punishments_punished` (`punished`), + INDEX `idx_punishments_ip` (`ip`(64)) +); + +CREATE TABLE IF NOT EXISTS `notes` ( + `row_id` BIGINT NOT NULL AUTO_INCREMENT, + `id` INT NOT NULL, + `uuid` VARCHAR(46) NOT NULL, + `written_by` VARCHAR(46), + `note` VARCHAR(2000), + `timestamp` BIGINT, + PRIMARY KEY (`row_id`), + INDEX `idx_notes_uuid` (`uuid`) +); + +CREATE TABLE IF NOT EXISTS `player_ips` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `player_uuid` VARCHAR(46) NOT NULL, + `ip` VARCHAR(64) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_player_ips_player_ip` (`player_uuid`, `ip`), + INDEX `idx_player_ips_ip` (`ip`) +); diff --git a/server/src/main/resources/db/migration/postgres/001_initial_schema.sql b/server/src/main/resources/db/migration/postgres/001_initial_schema.sql new file mode 100644 index 0000000..e6dbeb7 --- /dev/null +++ b/server/src/main/resources/db/migration/postgres/001_initial_schema.sql @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS players ( + uuid VARCHAR(46) NOT NULL PRIMARY KEY, + name VARCHAR(18), + login_msg VARCHAR(2000), + prefix VARCHAR(2000), + staffChat BOOLEAN, + ips VARCHAR(2000), + coins BIGINT, + vanished BOOLEAN, + commandspy BOOLEAN +); + +CREATE INDEX IF NOT EXISTS idx_players_name ON players(name); + +CREATE TABLE IF NOT EXISTS punishments ( + id BIGSERIAL PRIMARY KEY, + punished VARCHAR(46) NOT NULL, + punisher VARCHAR(46), + punisherName VARCHAR(64), + punishedUsername VARCHAR(16), + ip VARCHAR(2000), + type VARCHAR(30), + reason VARCHAR(2000), + customTime BOOLEAN, + active BOOLEAN, + issueDate BIGINT NOT NULL, + endDate BIGINT +); + +CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished); +CREATE INDEX IF NOT EXISTS idx_punishments_ip ON punishments(ip); + +CREATE TABLE IF NOT EXISTS notes ( + row_id BIGSERIAL PRIMARY KEY, + id INT NOT NULL, + uuid VARCHAR(46) NOT NULL, + written_by VARCHAR(46), + note VARCHAR(2000), + timestamp BIGINT +); + +CREATE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid); + +CREATE TABLE IF NOT EXISTS player_ips ( + id BIGSERIAL PRIMARY KEY, + player_uuid VARCHAR(46) NOT NULL, + ip VARCHAR(64) NOT NULL, + CONSTRAINT uq_player_ips_player_ip UNIQUE (player_uuid, ip) +); + +CREATE INDEX IF NOT EXISTS idx_player_ips_ip ON player_ips(ip); diff --git a/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql b/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql new file mode 100644 index 0000000..d4d23f1 --- /dev/null +++ b/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS players ( + uuid VARCHAR(46) NOT NULL PRIMARY KEY, + name VARCHAR(18), + login_msg VARCHAR(2000), + prefix VARCHAR(2000), + staffChat BOOLEAN, + ips VARCHAR(2000), + coins BIGINT, + vanished BOOLEAN, + commandspy BOOLEAN +); + +CREATE TABLE IF NOT EXISTS punishments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + punished VARCHAR(46) NOT NULL, + punisher VARCHAR(46), + punisherName VARCHAR(64), + punishedUsername VARCHAR(16), + ip VARCHAR(2000), + type VARCHAR(30), + reason VARCHAR(2000), + customTime BOOLEAN, + active BOOLEAN, + issueDate BIGINT NOT NULL, + endDate BIGINT +); + +CREATE TABLE IF NOT EXISTS notes ( + row_id INTEGER PRIMARY KEY AUTOINCREMENT, + id INT NOT NULL, + uuid VARCHAR(46) NOT NULL, + written_by VARCHAR(46), + note VARCHAR(2000), + timestamp BIGINT +); + +CREATE TABLE IF NOT EXISTS player_ips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_uuid VARCHAR(46) NOT NULL, + ip VARCHAR(64) NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_players_name ON players(name); +CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished); +CREATE INDEX IF NOT EXISTS idx_punishments_ip ON punishments(ip); +CREATE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid); +CREATE UNIQUE INDEX IF NOT EXISTS uq_player_ips_player_ip ON player_ips(player_uuid, ip); +CREATE INDEX IF NOT EXISTS idx_player_ips_ip ON player_ips(ip);