package de.ticketsystem.database; import java.io.File; import java.io.IOException; import org.bukkit.configuration.file.YamlConfiguration; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.Ticket; import de.ticketsystem.model.TicketStatus; import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.logging.Level; import java.io.FileReader; import java.io.FileWriter; import org.bukkit.Bukkit; public class DatabaseManager { // ─────────────────────────── Felder ──────────────────────────────────── private final TicketPlugin plugin; private HikariDataSource dataSource; private boolean useMySQL; private boolean useJson; private File dataFile; private YamlConfiguration dataConfig; private JSONArray dataJson; private String dataFileName; private String archiveFileName; // ─────────────────────────── Konstruktoren ───────────────────────────── public DatabaseManager(TicketPlugin plugin) { this.plugin = plugin; this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true); this.useJson = plugin.getConfig().getBoolean("use-json", false); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson); String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml"); String archivePath = plugin.getConfig().getString("archive-file", "archive.json"); this.dataFileName = dataPath; this.archiveFileName = archivePath; if (!useMySQL) { if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath); if (useJson) { dataFile = resolvePath(dataPath); if (!dataFile.exists()) { try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); dataJson = new JSONArray(); } catch (IOException e) { sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); } } else { try { JSONParser parser = new JSONParser(); dataJson = (JSONArray) parser.parse(new FileReader(dataFile)); } catch (Exception e) { sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); dataJson = new JSONArray(); } } } else { dataFile = resolvePath(dataPath); if (!dataFile.exists()) { try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); } catch (IOException e) { sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); } } dataConfig = YamlConfiguration.loadConfiguration(dataFile); } validateLoadedTickets(); } } // Konstruktor für Tests public DatabaseManager(File dataFile, YamlConfiguration dataConfig) { this.plugin = null; this.useMySQL = false; this.useJson = false; this.dataFileName = dataFile.getName(); this.archiveFileName = "archive.json"; this.dataFile = dataFile; this.dataConfig = dataConfig; validateLoadedTickets(); } // ─────────────────────────── Hilfsmethoden ───────────────────────────── private File resolvePath(String path) { File f = new File(path); if (f.isAbsolute()) return f; return new File(plugin != null ? plugin.getDataFolder() : new File("."), path); } private void sendError(String msg) { if (plugin != null) plugin.getLogger().severe(msg); if (Bukkit.getServer() != null) { Bukkit.getOnlinePlayers().stream() .filter(p -> p.hasPermission("ticket.admin")) .forEach(p -> p.sendMessage("§c[TicketSystem] " + msg)); } } // ─────────────────────────── Verbindung ──────────────────────────────── public boolean connect() { if (useMySQL) { try { HikariConfig config = new HikariConfig(); config.setJdbcUrl(String.format( "jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8", plugin.getConfig().getString("mysql.host"), plugin.getConfig().getInt("mysql.port"), plugin.getConfig().getString("mysql.database"))); config.setUsername(plugin.getConfig().getString("mysql.username")); config.setPassword(plugin.getConfig().getString("mysql.password")); config.setMaximumPoolSize(plugin.getConfig().getInt("mysql.pool-size", 10)); config.setConnectionTimeout(plugin.getConfig().getLong("mysql.connection-timeout", 30000)); config.setPoolName("TicketSystem-Pool"); config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); dataSource = new HikariDataSource(config); // Tabellen anlegen & fehlende Spalten ergänzen createTables(); ensureColumns(); plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); return true; } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!"); useMySQL = false; dataFile = new File(plugin.getDataFolder(), "data.yml"); if (!dataFile.exists()) { try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); } catch (IOException ex) { plugin.getLogger().severe("Konnte data.yml nicht erstellen: " + ex.getMessage()); } } dataConfig = YamlConfiguration.loadConfiguration(dataFile); return true; } } else { plugin.getLogger().info("MySQL deaktiviert. Verwende Datei-Speicherung (data.yml)."); return true; } } public void disconnect() { if (useMySQL && dataSource != null && !dataSource.isClosed()) { dataSource.close(); plugin.getLogger().info("MySQL-Verbindung getrennt."); } } private Connection getConnection() throws SQLException { return dataSource.getConnection(); } // ─────────────────────────── Tabellen erstellen ──────────────────────── private void createTables() { // close_comment ist jetzt von Anfang an in der CREATE-Anweisung enthalten String sql = """ CREATE TABLE IF NOT EXISTS tickets ( id INT AUTO_INCREMENT PRIMARY KEY, creator_uuid VARCHAR(36) NOT NULL, creator_name VARCHAR(16) NOT NULL, message VARCHAR(255) NOT NULL, world VARCHAR(64) NOT NULL, x DOUBLE NOT NULL, y DOUBLE NOT NULL, z DOUBLE NOT NULL, yaw FLOAT NOT NULL DEFAULT 0, pitch FLOAT NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'OPEN', claimer_uuid VARCHAR(36), claimer_name VARCHAR(16), forwarded_to_uuid VARCHAR(36), forwarded_to_name VARCHAR(16), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, claimed_at TIMESTAMP NULL, closed_at TIMESTAMP NULL, close_comment VARCHAR(500) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { stmt.execute(sql); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e); } } /** * Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch. * Wichtig für Server, die das Plugin bereits installiert hatten bevor * close_comment existierte. */ private void ensureColumns() { // close_comment hinzufügen, falls nicht vorhanden String checkSql = """ SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tickets' AND COLUMN_NAME = 'close_comment' """; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery(checkSql); if (rs.next() && rs.getInt(1) == 0) { // Spalte existiert nicht → hinzufügen stmt.execute("ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL"); plugin.getLogger().info("[TicketSystem] Spalte 'close_comment' wurde zur Datenbank hinzugefügt."); } } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(): " + e.getMessage(), e); } } // ─────────────────────────── CRUD ────────────────────────────────────── /** * Speichert ein neues Ticket in der DB und gibt die generierte ID zurück. */ public int createTicket(Ticket ticket) { if (useMySQL) { String sql = """ INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, ticket.getCreatorUUID().toString()); ps.setString(2, ticket.getCreatorName()); ps.setString(3, ticket.getMessage()); ps.setString(4, ticket.getWorldName()); ps.setDouble(5, ticket.getX()); ps.setDouble(6, ticket.getY()); ps.setDouble(7, ticket.getZ()); ps.setFloat(8, ticket.getYaw()); ps.setFloat(9, ticket.getPitch()); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { backupMySQL(); return rs.getInt(1); } } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen des Tickets: " + e.getMessage(), e); } return -1; } else { int id = dataConfig.getInt("lastId", 0) + 1; ticket.setId(id); dataConfig.set("lastId", id); dataConfig.set("tickets." + id, ticket); try { dataConfig.save(dataFile); backupDataFile(); } catch (IOException e) { plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); } return id; } } /** * Claimt ein Ticket (Status → CLAIMED). */ public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) { if (useMySQL) { String sql = """ UPDATE tickets SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?, claimed_at = NOW() WHERE id = ? AND status = 'OPEN' """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, claimerUUID.toString()); ps.setString(2, claimerName); ps.setInt(3, ticketId); return ps.executeUpdate() > 0; } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Claimen des Tickets: " + e.getMessage(), e); } return false; } else { Ticket t = getTicketById(ticketId); if (t == null || t.getStatus() != TicketStatus.OPEN) return false; t.setStatus(TicketStatus.CLAIMED); t.setClaimerUUID(claimerUUID); t.setClaimerName(claimerName); t.setClaimedAt(new Timestamp(System.currentTimeMillis())); dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); backupDataFile(); } catch (IOException e) { plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); } return true; } } /** * Schließt ein Ticket (Status → CLOSED). */ public boolean closeTicket(int ticketId, String closeComment) { if (useMySQL) { String sql = """ UPDATE tickets SET status = 'CLOSED', closed_at = NOW(), close_comment = ? WHERE id = ? AND status != 'CLOSED' """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, closeComment != null ? closeComment : ""); ps.setInt(2, ticketId); return ps.executeUpdate() > 0; } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), e); } return false; } else { Ticket t = getTicketById(ticketId); if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; t.setStatus(TicketStatus.CLOSED); t.setClosedAt(new Timestamp(System.currentTimeMillis())); t.setCloseComment(closeComment != null ? closeComment : ""); dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); backupDataFile(); } catch (IOException e) { plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); } return true; } } /** * Löscht ein Ticket anhand der ID. */ public boolean deleteTicket(int id) { if (useMySQL) { String sql = "DELETE FROM tickets WHERE id = ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); int rows = ps.executeUpdate(); if (rows > 0) { backupMySQL(); return true; } } catch (SQLException e) { sendError("Fehler beim Löschen des Tickets: " + e.getMessage()); } return false; } else { if (dataConfig.contains("tickets." + id)) { dataConfig.set("tickets." + id, null); try { dataConfig.save(dataFile); backupDataFile(); return true; } catch (IOException e) { sendError("Fehler beim Löschen des Tickets: " + e.getMessage()); } } return false; } } /** * Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED). */ public boolean forwardTicket(int ticketId, UUID toUUID, String toName) { if (useMySQL) { String sql = """ UPDATE tickets SET status = 'FORWARDED', forwarded_to_uuid = ?, forwarded_to_name = ? WHERE id = ? AND status != 'CLOSED' """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, toUUID.toString()); ps.setString(2, toName); ps.setInt(3, ticketId); return ps.executeUpdate() > 0; } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Weiterleiten des Tickets: " + e.getMessage(), e); } return false; } else { Ticket t = getTicketById(ticketId); if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; t.setStatus(TicketStatus.FORWARDED); t.setForwardedToUUID(toUUID); t.setForwardedToName(toName); dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); backupDataFile(); } catch (IOException e) { plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); } return true; } } // ─────────────────────────── Abfragen ────────────────────────────────── /** * Gibt alle Tickets mit einem bestimmten Status zurück. */ public List getTicketsByStatus(TicketStatus... statuses) { List list = new ArrayList<>(); if (statuses.length == 0) return list; if (useMySQL) { StringBuilder placeholders = new StringBuilder("?"); for (int i = 1; i < statuses.length; i++) placeholders.append(",?"); String sql = "SELECT * FROM tickets WHERE status IN (" + placeholders + ") ORDER BY created_at ASC"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < statuses.length; i++) ps.setString(i + 1, statuses[i].name()); ResultSet rs = ps.executeQuery(); while (rs.next()) list.add(mapRow(rs)); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen der Tickets: " + e.getMessage(), e); } return list; } else { if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); for (TicketStatus status : statuses) { if (t != null && t.getStatus() == status) list.add(t); } } } return list; } } /** * Gibt alle Tickets zurück (alle Status). */ public List getAllTickets() { List list = new ArrayList<>(); if (useMySQL) { try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); while (rs.next()) list.add(mapRow(rs)); } catch (SQLException e) { sendError("Fehler beim Abrufen aller Tickets: " + e.getMessage()); } } else { if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); if (t != null) list.add(t); } } } return list; } /** * Gibt ein einzelnes Ticket anhand der ID zurück. */ public Ticket getTicketById(int id) { if (useMySQL) { String sql = "SELECT * FROM tickets WHERE id = ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); ResultSet rs = ps.executeQuery(); if (rs.next()) return mapRow(rs); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen des Tickets: " + e.getMessage(), e); } return null; } else { if (dataConfig.contains("tickets." + id)) { return (Ticket) dataConfig.get("tickets." + id); } return null; } } /** * Anzahl offener Tickets (OPEN) – für Join-Benachrichtigung. */ public int countOpenTickets() { if (useMySQL) { String sql = "SELECT COUNT(*) FROM tickets WHERE status = 'OPEN'"; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery(sql); if (rs.next()) return rs.getInt(1); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Zählen der Tickets: " + e.getMessage(), e); } return 0; } else { int count = 0; if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); if (t != null && t.getStatus() == TicketStatus.OPEN) count++; } } return count; } } /** * Anzahl offener Tickets eines bestimmten Spielers. */ public int countOpenTicketsByPlayer(UUID uuid) { if (useMySQL) { String sql = "SELECT COUNT(*) FROM tickets WHERE creator_uuid = ? AND status IN ('OPEN', 'CLAIMED', 'FORWARDED')"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, uuid.toString()); ResultSet rs = ps.executeQuery(); if (rs.next()) return rs.getInt(1); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler: " + e.getMessage(), e); } return 0; } else { int count = 0; if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); if (t != null && uuid.equals(t.getCreatorUUID()) && (t.getStatus() == TicketStatus.OPEN || t.getStatus() == TicketStatus.CLAIMED || t.getStatus() == TicketStatus.FORWARDED)) count++; } } return count; } } // ─────────────────────────── Archivierung ────────────────────────────── /** * Archiviert alle geschlossenen Tickets in eine separate Datei. */ public int archiveClosedTickets() { List all = getAllTickets(); List toArchive = new ArrayList<>(); for (Ticket t : all) { if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t); } if (toArchive.isEmpty()) return 0; File archiveFile = new File(plugin.getDataFolder(), archiveFileName); JSONArray arr = new JSONArray(); if (archiveFile.exists()) { try (FileReader fr = new FileReader(archiveFile)) { JSONParser parser = new JSONParser(); Object parsed = parser.parse(fr); if (parsed instanceof JSONArray oldArr) arr.addAll(oldArr); } catch (Exception ignored) {} } for (Ticket t : toArchive) arr.add(ticketToJson(t)); try (FileWriter fw = new FileWriter(archiveFile)) { fw.write(arr.toJSONString()); } catch (Exception e) { sendError("Fehler beim Archivieren: " + e.getMessage()); return 0; } int removed = 0; if (useMySQL) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement("DELETE FROM tickets WHERE id = ?")) { for (Ticket t : toArchive) { ps.setInt(1, t.getId()); ps.executeUpdate(); removed++; } } catch (Exception e) { sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage()); } } else { for (Ticket t : toArchive) { dataConfig.set("tickets." + t.getId(), null); removed++; } try { dataConfig.save(dataFile); } catch (Exception e) { sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage()); } } return removed; } // ─────────────────────────── Statistiken ─────────────────────────────── public TicketStats getTicketStats() { List all = getAllTickets(); int open = 0, claimed = 0, forwarded = 0, closed = 0; java.util.Map byPlayer = new java.util.HashMap<>(); for (Ticket t : all) { switch (t.getStatus()) { case OPEN -> open++; case CLAIMED -> claimed++; case FORWARDED -> forwarded++; case CLOSED -> closed++; } byPlayer.merge(t.getCreatorName(), 1, Integer::sum); } return new TicketStats(all.size(), open, closed, forwarded, byPlayer); } public static class TicketStats { public final int total, open, closed, forwarded; public final java.util.Map byPlayer; public TicketStats(int total, int open, int closed, int forwarded, java.util.Map byPlayer) { this.total = total; this.open = open; this.closed = closed; this.forwarded = forwarded; this.byPlayer = byPlayer; } } // ─────────────────────────── Export / Import ──────────────────────────── public int exportTickets(File exportFile) { List tickets = getAllTickets(); JSONArray arr = new JSONArray(); for (Ticket t : tickets) arr.add(ticketToJson(t)); try (FileWriter fw = new FileWriter(exportFile)) { fw.write(arr.toJSONString()); return tickets.size(); } catch (IOException e) { sendError("Fehler beim Export: " + e.getMessage()); return 0; } } public int importTickets(File importFile) { int imported = 0; try (FileReader fr = new FileReader(importFile)) { JSONParser parser = new JSONParser(); JSONArray arr = (JSONArray) parser.parse(fr); for (Object o : arr) { Ticket t = ticketFromJson((JSONObject) o); if (t != null && createTicket(t) != -1) imported++; } } catch (Exception e) { sendError("Fehler beim Import: " + e.getMessage()); } return imported; } // ─────────────────────────── Migration ───────────────────────────────── public int migrateToMySQL() { if (useMySQL || dataConfig == null) return 0; int migrated = 0; try { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); if (t != null) { useMySQL = true; int id = createTicket(t); useMySQL = false; if (id != -1) migrated++; } } } catch (Exception e) { plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage()); } return migrated; } public int migrateToFile() { if (!useMySQL) return 0; int migrated = 0; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); while (rs.next()) { Ticket t = mapRow(rs); if (t != null) { useMySQL = false; int id = createTicket(t); useMySQL = true; if (id != -1) migrated++; } } } catch (Exception e) { plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); } return migrated; } // ─────────────────────────── Mapping ─────────────────────────────────── /** * Liest eine Zeile aus dem ResultSet und erstellt ein Ticket-Objekt. * close_comment wird mit try-catch abgesichert, damit ältere Datenbanken * ohne diese Spalte nicht abstürzen. */ private Ticket mapRow(ResultSet rs) throws SQLException { Ticket t = new Ticket(); t.setId(rs.getInt("id")); t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid"))); t.setCreatorName(rs.getString("creator_name")); t.setMessage(rs.getString("message")); t.setWorldName(rs.getString("world")); t.setX(rs.getDouble("x")); t.setY(rs.getDouble("y")); t.setZ(rs.getDouble("z")); t.setYaw(rs.getFloat("yaw")); t.setPitch(rs.getFloat("pitch")); t.setStatus(TicketStatus.valueOf(rs.getString("status"))); t.setCreatedAt(rs.getTimestamp("created_at")); t.setClaimedAt(rs.getTimestamp("claimed_at")); t.setClosedAt(rs.getTimestamp("closed_at")); // ── BUGFIX: close_comment mit try-catch absichern ────────────────── // Wenn die Spalte in einer alten DB noch nicht existiert, wird der // Fehler ignoriert statt die gesamte Ticket-Liste leer zu lassen. try { String closeComment = rs.getString("close_comment"); if (closeComment != null) t.setCloseComment(closeComment); } catch (SQLException ignored) { // Spalte existiert noch nicht – ensureColumns() ergänzt sie beim nächsten Start } String claimerUUID = rs.getString("claimer_uuid"); if (claimerUUID != null) { t.setClaimerUUID(UUID.fromString(claimerUUID)); t.setClaimerName(rs.getString("claimer_name")); } String fwdUUID = rs.getString("forwarded_to_uuid"); if (fwdUUID != null) { t.setForwardedToUUID(UUID.fromString(fwdUUID)); t.setForwardedToName(rs.getString("forwarded_to_name")); } return t; } // ─────────────────────────── JSON-Hilfsmethoden ───────────────────────── private JSONObject ticketToJson(Ticket t) { JSONObject obj = new JSONObject(); obj.put("id", t.getId()); obj.put("creatorUUID", t.getCreatorUUID().toString()); obj.put("creatorName", t.getCreatorName()); obj.put("message", t.getMessage()); obj.put("world", t.getWorldName()); obj.put("x", t.getX()); obj.put("y", t.getY()); obj.put("z", t.getZ()); obj.put("yaw", t.getYaw()); obj.put("pitch", t.getPitch()); obj.put("status", t.getStatus().name()); obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null); obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null); obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null); if (t.getClaimerUUID() != null) obj.put("claimerUUID", t.getClaimerUUID().toString()); if (t.getClaimerName() != null) obj.put("claimerName", t.getClaimerName()); if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString()); if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName()); if (t.getCloseComment() != null) obj.put("closeComment", t.getCloseComment()); return obj; } private Ticket ticketFromJson(JSONObject obj) { try { Ticket t = new Ticket(); t.setId(((Long) obj.get("id")).intValue()); t.setCreatorUUID(UUID.fromString((String) obj.get("creatorUUID"))); t.setCreatorName((String) obj.get("creatorName")); t.setMessage((String) obj.get("message")); t.setWorldName((String) obj.get("world")); t.setX((Double) obj.get("x")); t.setY((Double) obj.get("y")); t.setZ((Double) obj.get("z")); t.setYaw(((Double) obj.get("yaw")).floatValue()); t.setPitch(((Double) obj.get("pitch")).floatValue()); t.setStatus(TicketStatus.valueOf((String) obj.get("status"))); if (obj.get("createdAt") != null) t.setCreatedAt(new Timestamp((Long) obj.get("createdAt"))); if (obj.get("claimedAt") != null) t.setClaimedAt(new Timestamp((Long) obj.get("claimedAt"))); if (obj.get("closedAt") != null) t.setClosedAt(new Timestamp((Long) obj.get("closedAt"))); if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String) obj.get("claimerUUID"))); if (obj.get("claimerName") != null) t.setClaimerName((String) obj.get("claimerName")); if (obj.get("forwardedToUUID") != null) t.setForwardedToUUID(UUID.fromString((String) obj.get("forwardedToUUID"))); if (obj.get("forwardedToName") != null) t.setForwardedToName((String) obj.get("forwardedToName")); if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment")); return t; } catch (Exception e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); return null; } } // ─────────────────────────── Validierung ─────────────────────────────── private void validateLoadedTickets() { if (dataConfig == null || !dataConfig.contains("tickets")) return; int invalid = 0; for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Object obj = dataConfig.get("tickets." + key); if (!(obj instanceof Ticket t)) { sendError("Ungültiges Ticket-Objekt bei ID: " + key); invalid++; continue; } if (t.getCreatorUUID() == null || t.getCreatorName() == null || t.getMessage() == null || t.getStatus() == null) { sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key); invalid++; } try { UUID.fromString(t.getCreatorUUID().toString()); } catch (Exception e) { sendError("Ungültige UUID bei Ticket ID: " + key); invalid++; } try { TicketStatus.valueOf(t.getStatus().name()); } catch (Exception e) { sendError("Ungültiger Status bei Ticket ID: " + key); invalid++; } } if (invalid > 0) { String msg = plugin != null ? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid)) : invalid + " ungültige Tickets beim Laden gefunden."; sendError(msg); } } // ─────────────────────────── Backup (Platzhalter) ────────────────────── private void backupMySQL() { // TODO: MySQL-Backup implementieren } private void backupDataFile() { // TODO: Datei-Backup implementieren } }