Files
TicketSystem/src/main/java/de/ticketsystem/database/DatabaseManager.java
2026-02-20 12:31:38 +01:00

848 lines
38 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Ticket> getTicketsByStatus(TicketStatus... statuses) {
List<Ticket> 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<Ticket> getAllTickets() {
List<Ticket> 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<Ticket> all = getAllTickets();
List<Ticket> 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<Ticket> all = getAllTickets();
int open = 0, claimed = 0, forwarded = 0, closed = 0;
java.util.Map<String, Integer> 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<String, Integer> byPlayer;
public TicketStats(int total, int open, int closed, int forwarded, java.util.Map<String, Integer> byPlayer) {
this.total = total; this.open = open; this.closed = closed;
this.forwarded = forwarded; this.byPlayer = byPlayer;
}
}
// ─────────────────────────── Export / Import ────────────────────────────
public int exportTickets(File exportFile) {
List<Ticket> 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
}
}