From d3909f9dbc877c583c26aed8e7d13e009c9977e3 Mon Sep 17 00:00:00 2001 From: Tristan Vermeesch Date: Sat, 13 Dec 2025 15:23:52 +0100 Subject: [PATCH 1/2] feat: move execution data for task to different file --- .../playbosswar/com/CommandTimerPlugin.java | 2 + .../playbosswar/com/commands/MainCommand.java | 28 +++- .../com/language/LanguageManager.java | 4 +- .../com/tasks/AdHocCommandsManager.java | 4 +- .../java/me/playbosswar/com/tasks/Task.java | 18 ++- .../com/tasks/TaskExecutionMetadata.java | 14 +- .../me/playbosswar/com/tasks/TaskRunner.java | 2 - .../playbosswar/com/tasks/TasksManager.java | 4 - .../java/me/playbosswar/com/utils/Files.java | 46 +++--- .../ExecutionMetadataMigration.java | 89 +++++++++++ .../com/utils/migrations/Migration.java | 12 ++ .../utils/migrations/MigrationManager.java | 138 ++++++++++++++++++ 12 files changed, 322 insertions(+), 39 deletions(-) create mode 100644 src/main/java/me/playbosswar/com/utils/migrations/ExecutionMetadataMigration.java create mode 100644 src/main/java/me/playbosswar/com/utils/migrations/Migration.java create mode 100644 src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java diff --git a/src/main/java/me/playbosswar/com/CommandTimerPlugin.java b/src/main/java/me/playbosswar/com/CommandTimerPlugin.java index 5f086181..f7a83ebc 100644 --- a/src/main/java/me/playbosswar/com/CommandTimerPlugin.java +++ b/src/main/java/me/playbosswar/com/CommandTimerPlugin.java @@ -26,6 +26,7 @@ import me.playbosswar.com.updater.Updater; import me.playbosswar.com.utils.Files; import me.playbosswar.com.utils.Messages; +import me.playbosswar.com.utils.migrations.MigrationManager; import me.playbosswar.com.utils.Tools; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; @@ -88,6 +89,7 @@ public void onEnable() { Bukkit.getPluginManager().registerEvents(new JoinEvents(), this); Files.migrateFileNamesToFileUuids(); + new MigrationManager(this).runMigrations(); if(getConfig().getBoolean("database.enabled")) { try { Class.forName("com.mysql.jdbc.Driver"); diff --git a/src/main/java/me/playbosswar/com/commands/MainCommand.java b/src/main/java/me/playbosswar/com/commands/MainCommand.java index 38a3c50c..baee9da9 100644 --- a/src/main/java/me/playbosswar/com/commands/MainCommand.java +++ b/src/main/java/me/playbosswar/com/commands/MainCommand.java @@ -15,6 +15,7 @@ import me.playbosswar.com.utils.Files; import me.playbosswar.com.utils.Messages; import me.playbosswar.com.utils.Tools; +import me.playbosswar.com.utils.migrations.MigrationManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -184,8 +185,33 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @N if(args.length == 2) { String action = args[0]; - String taskName = args[1]; + if(action.equalsIgnoreCase("rollback")) { + if(!sender.hasPermission("commandtimer.manage")) { + Messages.sendNoPermission(sender); + return true; + } + + try { + int targetVersion = Integer.parseInt(args[1]); + MigrationManager migrationManager = new MigrationManager(CommandTimerPlugin.getInstance()); + + if(targetVersion > migrationManager.getCurrentVersion()) { + Messages.sendMessage(sender, "&cCannot rollback to version " + targetVersion + ". Current version is " + migrationManager.getCurrentVersion()); + return true; + } + + Messages.sendMessage(sender, "&eStarting rollback to version " + targetVersion + "..."); + migrationManager.rollbackToVersion(targetVersion); + Messages.sendMessage(sender, "&aRollback complete. Please restart the server for changes to take effect."); + return true; + } catch(NumberFormatException e) { + Messages.sendMessage(sender, "&cInvalid version number: " + args[1]); + return true; + } + } + + String taskName = args[1]; Task task = tasksManager.getTaskByName(taskName); if(task == null) { Messages.sendMessage(sender, languageManager.get(LanguageKey.NO_TASK)); diff --git a/src/main/java/me/playbosswar/com/language/LanguageManager.java b/src/main/java/me/playbosswar/com/language/LanguageManager.java index abfcae9e..6f4682d5 100644 --- a/src/main/java/me/playbosswar/com/language/LanguageManager.java +++ b/src/main/java/me/playbosswar/com/language/LanguageManager.java @@ -58,9 +58,7 @@ private void validateConfiguration() throws Exception { } if (fileSaveRequired) { - try { - String filePath = getLanguageFilePath(selectedLanguage); - FileWriter jsonFile = new FileWriter(filePath); + try (FileWriter jsonFile = new FileWriter(getLanguageFilePath(selectedLanguage))) { jsonFile.write(selectedLanguageObject.toJSONString()); jsonFile.flush(); } catch (IOException e) { diff --git a/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java b/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java index 3cb705cb..d9120762 100644 --- a/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java +++ b/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java @@ -123,11 +123,9 @@ private List loadAdHocCommandsFromFiles() { private void storeCommand(AdHocCommand command) { GsonConverter gson = new GsonConverter(); String json = gson.toJson(command); - try { - FileWriter jsonFile = new FileWriter(Files.getAdHocCommandFile(command.getId())); + try (FileWriter jsonFile = new FileWriter(Files.getAdHocCommandFile(command.getId()))) { jsonFile.write(json); jsonFile.flush(); - jsonFile.close(); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/me/playbosswar/com/tasks/Task.java b/src/main/java/me/playbosswar/com/tasks/Task.java index 1307ec95..77d0c83e 100644 --- a/src/main/java/me/playbosswar/com/tasks/Task.java +++ b/src/main/java/me/playbosswar/com/tasks/Task.java @@ -40,9 +40,9 @@ public class Task { private Collection days = new ArrayList<>(); @DatabaseField private int executionLimit = -1; - private int timesExecuted = 0; - private int lastExecutedCommandIndex = 0; - private Date lastExecuted = new Date(); + private transient int timesExecuted = 0; + private transient int lastExecutedCommandIndex = 0; + private transient Date lastExecuted = new Date(); @DatabaseField private CommandExecutionMode commandExecutionMode = CommandExecutionMode.ALL; @DatabaseField(persisterClass = TaskIntervalPersistor.class) @@ -184,6 +184,7 @@ public int getTimesExecuted() { public void setTimesExecuted(int timesExecuted) { this.timesExecuted = timesExecuted; + storeExecutionMetadata(); } public Date getLastExecuted() { @@ -192,6 +193,7 @@ public Date getLastExecuted() { public void setLastExecuted(Date lastExecuted) { this.lastExecuted = lastExecuted; + storeExecutionMetadata(); } public boolean isActive() { @@ -264,6 +266,7 @@ public int getLastExecutedCommandIndex() { public void setLastExecutedCommandIndex(int lastExecutedCommandIndex) { this.lastExecutedCommandIndex = lastExecutedCommandIndex; + storeExecutionMetadata(); } public Condition getCondition() { @@ -320,6 +323,10 @@ public void setId(UUID id) { this.id = id; } + public void storeExecutionMetadata() { + Files.updateLocalTaskMetadata(this); + } + public void storeInstance() { if(CommandTimerPlugin.getInstance().getConfig().getBoolean("database.enabled")) { try { @@ -335,11 +342,10 @@ public void storeInstance() { String json = gson.toJson(this); transaction.setContext("task", json); - try { - FileWriter jsonFile = new FileWriter(Files.getTaskFile(id)); + try (FileWriter jsonFile = new FileWriter(Files.getTaskFile(id))) { jsonFile.write(json); jsonFile.flush(); - } catch(IOException e) { + } catch (IOException e) { e.printStackTrace(); transaction.setThrowable(e); transaction.setStatus(SpanStatus.INTERNAL_ERROR); diff --git a/src/main/java/me/playbosswar/com/tasks/TaskExecutionMetadata.java b/src/main/java/me/playbosswar/com/tasks/TaskExecutionMetadata.java index e443afc9..e8d50b55 100644 --- a/src/main/java/me/playbosswar/com/tasks/TaskExecutionMetadata.java +++ b/src/main/java/me/playbosswar/com/tasks/TaskExecutionMetadata.java @@ -1,19 +1,29 @@ package me.playbosswar.com.tasks; import java.util.Date; +import java.util.UUID; -// Data class only used to store metadata in local json files public class TaskExecutionMetadata { + private UUID taskId; private int timesExecuted = 0; private int lastExecutedCommandIndex = 0; private Date lastExecuted = new Date(); - public TaskExecutionMetadata(int timesExecuted, int lastExecutedCommandIndex, Date lastExecuted) { + public TaskExecutionMetadata(UUID taskId, int timesExecuted, int lastExecutedCommandIndex, Date lastExecuted) { + this.taskId = taskId; this.timesExecuted = timesExecuted; this.lastExecutedCommandIndex = lastExecutedCommandIndex; this.lastExecuted = lastExecuted; } + public UUID getTaskId() { + return taskId; + } + + public void setTaskId(UUID taskId) { + this.taskId = taskId; + } + public int getTimesExecuted() { return timesExecuted; } diff --git a/src/main/java/me/playbosswar/com/tasks/TaskRunner.java b/src/main/java/me/playbosswar/com/tasks/TaskRunner.java index 31d43ab3..6218611f 100644 --- a/src/main/java/me/playbosswar/com/tasks/TaskRunner.java +++ b/src/main/java/me/playbosswar/com/tasks/TaskRunner.java @@ -65,8 +65,6 @@ private void processTask(Task task) { task.setLastExecutedCommandIndex(task.getCommands().indexOf(taskCommand)); CommandTimerPlugin.getScheduler().runTask(() -> tasksManager.processCommandExecution(task, taskCommand)); } - - task.storeInstance(); } @Override diff --git a/src/main/java/me/playbosswar/com/tasks/TasksManager.java b/src/main/java/me/playbosswar/com/tasks/TasksManager.java index c233fe90..2e939ba1 100644 --- a/src/main/java/me/playbosswar/com/tasks/TasksManager.java +++ b/src/main/java/me/playbosswar/com/tasks/TasksManager.java @@ -59,7 +59,6 @@ public TasksManager(Plugin plugin) { if (task.isResetExecutionsAfterRestart()) { task.setTimesExecuted(0); task.setLastExecuted(new Date()); - task.storeInstance(); } }); @@ -159,7 +158,6 @@ public void processCommandExecution(Task task, TaskCommand taskCommand) { if (executed) { task.setLastExecuted(new Date()); task.setTimesExecuted(task.getTimesExecuted() + 1); - task.storeInstance(); } }); } @@ -409,8 +407,6 @@ private boolean isTimeInRange(LocalTime time, LocalTime start, LocalTime end) { } public void disable() { - List tasksToStore = loadedTasks.stream().filter(Task::isActive).collect(Collectors.toList()); - tasksToStore.forEach(Task::storeInstance); stopRunner = true; if (runnerThread != null && runnerThread.isAlive()) { runnerThread.interrupt(); diff --git a/src/main/java/me/playbosswar/com/utils/Files.java b/src/main/java/me/playbosswar/com/utils/Files.java index 9c960aaf..f0dc1340 100644 --- a/src/main/java/me/playbosswar/com/utils/Files.java +++ b/src/main/java/me/playbosswar/com/utils/Files.java @@ -38,9 +38,11 @@ public static void createDataFolders() { File timersFile = new File(pluginFolderPath + "/timers"); File extensionsFolder = new File(pluginFolderPath + "/extensions"); File adHocCommandsFolder = new File(pluginFolderPath + "/ad-hoc-commands"); + File executionDataFolder = new File(pluginFolderPath + "/execution-data"); timersFile.mkdirs(); extensionsFolder.mkdirs(); adHocCommandsFolder.mkdirs(); + executionDataFolder.mkdirs(); File dataFolder = CommandTimerPlugin.getPlugin().getDataFolder(); File enLangFile = new File(dataFolder.getAbsoluteFile() + "/languages/en.json"); @@ -59,7 +61,7 @@ public static String getTaskFile(UUID id) { } public static String getTaskLocalExecutionFile(UUID id) { - return pluginFolderPath + "/timers/" + id + ".local.json"; + return pluginFolderPath + "/execution-data/" + id + ".json"; } public static String getAdHocCommandsDirectory() { @@ -91,7 +93,7 @@ private static void healTask(Task task) { public static void migrateFileNamesToFileUuids() { File dir = new File(pluginFolderPath + "/timers"); - File[] directoryListing = dir.listFiles(file -> !file.getName().contains(".local.json")); + File[] directoryListing = dir.listFiles(file -> file.getName().endsWith(".json")); if(directoryListing != null) { for(File file : directoryListing) { @@ -112,9 +114,10 @@ public static void migrateFileNamesToFileUuids() { GsonConverter gson = new GsonConverter(); String json = gson.toJson(task); - FileWriter jsonFile = new FileWriter(pluginFolderPath + "/timers/" + uuid + ".json"); - jsonFile.write(json); - jsonFile.flush(); + try (FileWriter jsonFile = new FileWriter(pluginFolderPath + "/timers/" + uuid + ".json")) { + jsonFile.write(json); + jsonFile.flush(); + } file.delete(); Messages.sendConsole("Migrated " + file.getName() + " to " + uuid + ".json"); @@ -130,14 +133,15 @@ public static TaskExecutionMetadata getOrCreateTaskMetadata(Task task) { try { String path = getTaskLocalExecutionFile(task.getId()); File file = new File(path); - if(!file.exists()) { - TaskExecutionMetadata metadata = new TaskExecutionMetadata(task.getTimesExecuted(), - task.getLastExecutedCommandIndex(), task.getLastExecuted()); + if (!file.exists()) { + TaskExecutionMetadata metadata = new TaskExecutionMetadata(task.getId(), + task.getTimesExecuted(), task.getLastExecutedCommandIndex(), task.getLastExecuted()); GsonConverter gson = new GsonConverter(); String json = gson.toJson(metadata); - FileWriter jsonFile = new FileWriter(path); - jsonFile.write(json); - jsonFile.flush(); + try (FileWriter jsonFile = new FileWriter(path)) { + jsonFile.write(json); + jsonFile.flush(); + } return metadata; } @@ -153,16 +157,15 @@ public static TaskExecutionMetadata getOrCreateTaskMetadata(Task task) { } public static void updateLocalTaskMetadata(Task task) { - TaskExecutionMetadata metadata = new TaskExecutionMetadata(task.getTimesExecuted(), - task.getLastExecutedCommandIndex(), task.getLastExecuted()); + TaskExecutionMetadata metadata = new TaskExecutionMetadata(task.getId(), + task.getTimesExecuted(), task.getLastExecutedCommandIndex(), task.getLastExecuted()); GsonConverter gson = new GsonConverter(); String json = gson.toJson(metadata); - try { - FileWriter jsonFile = new FileWriter(getTaskLocalExecutionFile(task.getId())); + try (FileWriter jsonFile = new FileWriter(getTaskLocalExecutionFile(task.getId()))) { jsonFile.write(json); jsonFile.flush(); - } catch(IOException e) { + } catch (IOException e) { e.printStackTrace(); } } @@ -171,7 +174,7 @@ public static List deserializeJsonFilesIntoCommandTimers() { ITransaction transaction = Sentry.startTransaction("deserializeJsonFilesIntoCommandTimers()", "initiation"); File dir = new File(pluginFolderPath + "/timers"); - File[] directoryListing = dir.listFiles(file -> !file.getName().contains(".local.json")); + File[] directoryListing = dir.listFiles(file -> file.getName().endsWith(".json")); JSONParser jsonParser = new JSONParser(); List tasks = new ArrayList<>(); @@ -183,7 +186,7 @@ public static List deserializeJsonFilesIntoCommandTimers() { } try { - Messages.sendConsole("Processing task " + file.getName()); + Messages.sendConsole("Loading task " + file.getName()); FileReader fr = new FileReader(file.getPath()); GsonConverter gson = new GsonConverter(); @@ -202,6 +205,13 @@ public static List deserializeJsonFilesIntoCommandTimers() { task.setEvents(new ArrayList<>()); } + TaskExecutionMetadata metadata = getOrCreateTaskMetadata(task); + if (metadata != null) { + task.setTimesExecuted(metadata.getTimesExecuted()); + task.setLastExecutedCommandIndex(metadata.getLastExecutedCommandIndex()); + task.setLastExecuted(metadata.getLastExecuted()); + } + tasks.add(task); } catch (JsonParseException e) { Bukkit.getLogger().log(Level.SEVERE, "Failed to process " + file.getName() + " because of " + e.getMessage()); diff --git a/src/main/java/me/playbosswar/com/utils/migrations/ExecutionMetadataMigration.java b/src/main/java/me/playbosswar/com/utils/migrations/ExecutionMetadataMigration.java new file mode 100644 index 00000000..04d43f1c --- /dev/null +++ b/src/main/java/me/playbosswar/com/utils/migrations/ExecutionMetadataMigration.java @@ -0,0 +1,89 @@ +package me.playbosswar.com.utils.migrations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import me.playbosswar.com.tasks.TaskExecutionMetadata; +import me.playbosswar.com.utils.Files; +import me.playbosswar.com.utils.gson.GsonConverter; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +public class ExecutionMetadataMigration implements Migration { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss. SSSXXX"); + + @Override + public int getVersion() { + return 1; + } + + @Override + public String getDescription() { + return "Move execution metadata to separate files"; + } + + @Override + public void migrate(File taskFile, JsonObject taskJson) throws Exception { + String idStr = taskJson.get("id").getAsString(); + UUID id = UUID.fromString(idStr); + + if (!taskJson.has("timesExecuted") || !taskJson.has("lastExecutedCommandIndex") || !taskJson.has("lastExecuted")) { + throw new IllegalStateException("Missing required fields for migration: timesExecuted, lastExecutedCommandIndex, or lastExecuted"); + } + + int timesExecuted = taskJson.get("timesExecuted").getAsInt(); + int lastExecutedCommandIndex = taskJson.get("lastExecutedCommandIndex").getAsInt(); + Date lastExecuted = DATE_FORMAT.parse(taskJson.get("lastExecuted").getAsString()); + + File metadataFile = new File(Files.getTaskLocalExecutionFile(id)); + if (!metadataFile.exists()) { + TaskExecutionMetadata metadata = new TaskExecutionMetadata(id, timesExecuted, lastExecutedCommandIndex, lastExecuted); + GsonConverter gson = new GsonConverter(); + try (FileWriter metaWriter = new FileWriter(metadataFile)) { + metaWriter.write(gson.toJson(metadata)); + metaWriter.flush(); + } + } + + taskJson.remove("timesExecuted"); + taskJson.remove("lastExecutedCommandIndex"); + taskJson.remove("lastExecuted"); + } + + @Override + public void rollback(File taskFile, JsonObject taskJson) throws Exception { + String idStr = taskJson.get("id").getAsString(); + UUID id = UUID.fromString(idStr); + + File metadataFile = new File(Files.getTaskLocalExecutionFile(id)); + if (!metadataFile.exists()) { + taskJson.addProperty("timesExecuted", 0); + taskJson.addProperty("lastExecutedCommandIndex", 0); + taskJson.addProperty("lastExecuted", DATE_FORMAT.format(new Date())); + return; + } + + try (FileReader fr = new FileReader(metadataFile)) { + JsonObject metadataJson = new JsonParser().parse(fr).getAsJsonObject(); + + if (!metadataJson.has("timesExecuted") || !metadataJson.has("lastExecutedCommandIndex") || !metadataJson.has("lastExecuted")) { + throw new IllegalStateException("Missing required fields in metadata file: timesExecuted, lastExecutedCommandIndex, or lastExecuted"); + } + + int timesExecuted = metadataJson.get("timesExecuted").getAsInt(); + int lastExecutedCommandIndex = metadataJson.get("lastExecutedCommandIndex").getAsInt(); + String lastExecuted = metadataJson.get("lastExecuted").getAsString(); + + taskJson.addProperty("timesExecuted", timesExecuted); + taskJson.addProperty("lastExecutedCommandIndex", lastExecutedCommandIndex); + taskJson.addProperty("lastExecuted", lastExecuted); + } + + metadataFile.delete(); + } +} + diff --git a/src/main/java/me/playbosswar/com/utils/migrations/Migration.java b/src/main/java/me/playbosswar/com/utils/migrations/Migration.java new file mode 100644 index 00000000..2e840fb0 --- /dev/null +++ b/src/main/java/me/playbosswar/com/utils/migrations/Migration.java @@ -0,0 +1,12 @@ +package me.playbosswar.com.utils.migrations; + +import com.google.gson.JsonObject; +import java.io.File; + +public interface Migration { + int getVersion(); + String getDescription(); + void migrate(File taskFile, JsonObject taskJson) throws Exception; + void rollback(File taskFile, JsonObject taskJson) throws Exception; +} + diff --git a/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java b/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java new file mode 100644 index 00000000..b140536f --- /dev/null +++ b/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java @@ -0,0 +1,138 @@ +package me.playbosswar.com.utils.migrations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import me.playbosswar.com.CommandTimerPlugin; +import me.playbosswar.com.utils.Messages; +import me.playbosswar.com.utils.gson.GsonConverter; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class MigrationManager { + public static final int CURRENT_VERSION = 1; + private final List migrations = new ArrayList<>(); + private final String pluginFolderPath; + + public MigrationManager(CommandTimerPlugin plugin) { + this.pluginFolderPath = plugin.getDataFolder().getPath(); + registerMigrations(); + } + + private void registerMigrations() { + migrations.add(new ExecutionMetadataMigration()); + } + + public void runMigrations() { + File dir = new File(pluginFolderPath + "/timers"); + File[] files = dir.listFiles(file -> file.getName().endsWith(".json")); + if (files == null) return; + + for (File file : files) { + try (FileReader fr = new FileReader(file)) { + JsonObject json = new JsonParser().parse(fr).getAsJsonObject(); + + int fileVersion = json.has("configVersion") ? json.get("configVersion").getAsInt() : 0; + + if (fileVersion >= CURRENT_VERSION) { + Messages.sendDebugConsole("Skipping migration for " + file.getName() + " (v" + fileVersion + " >= v" + CURRENT_VERSION + ")"); + continue; + } + + Messages.sendConsole("Migrating task file: " + file.getName() + " (v" + fileVersion + " -> v" + CURRENT_VERSION + ")"); + + for (Migration migration : migrations.stream() + .filter(m -> m.getVersion() > fileVersion) + .sorted(Comparator.comparingInt(Migration::getVersion)) + .toArray(Migration[]::new)) { + try { + migration.migrate(file, json); + } catch (Exception e) { + Messages.sendConsole("Migration v" + migration.getVersion() + " failed for " + file.getName() + ": " + e.getMessage()); + e.printStackTrace(); + } + } + + Messages.sendConsole("Migration complete for " + file.getName() + " (v" + fileVersion + " -> v" + CURRENT_VERSION + ")"); + + json.addProperty("configVersion", CURRENT_VERSION); + GsonConverter gson = new GsonConverter(); + try (FileWriter fw = new FileWriter(file)) { + fw.write(gson.toJson(json)); + fw.flush(); + } + + } catch (IOException e) { + Messages.sendConsole("Failed to process " + file.getName() + ": " + e.getMessage()); + } + } + } + + public void rollbackToVersion(int targetVersion) { + if (targetVersion < 0) { + Messages.sendConsole("Invalid target version: " + targetVersion); + return; + } + + File dir = new File(pluginFolderPath + "/timers"); + File[] files = dir.listFiles(file -> file.getName().endsWith(".json")); + if (files == null) return; + + for (File file : files) { + try (FileReader fr = new FileReader(file)) { + JsonObject json = new JsonParser().parse(fr).getAsJsonObject(); + + int fileVersion = json.has("configVersion") ? json.get("configVersion").getAsInt() : 0; + + if (fileVersion <= targetVersion) { + continue; + } + + Messages.sendConsole("Rolling back task file: " + file.getName() + " (v" + fileVersion + " -> v" + targetVersion + ")"); + + for (Migration migration : migrations.stream() + .filter(m -> m.getVersion() <= fileVersion && m.getVersion() > targetVersion) + .sorted((a, b) -> Integer.compare(b.getVersion(), a.getVersion())) + .toArray(Migration[]::new)) { + try { + Messages.sendConsole("Rolling back migration v" + migration.getVersion() + ": " + migration.getDescription()); + migration.rollback(file, json); + } catch (Exception e) { + Messages.sendConsole("Rollback v" + migration.getVersion() + " failed for " + file.getName() + ": " + e.getMessage()); + e.printStackTrace(); + } + } + + if (targetVersion == 0) { + json.remove("configVersion"); + } else { + json.addProperty("configVersion", targetVersion); + } + GsonConverter gson = new GsonConverter(); + try (FileWriter fw = new FileWriter(file)) { + fw.write(gson.toJson(json)); + fw.flush(); + } + + } catch (IOException e) { + Messages.sendConsole("Failed to process " + file.getName() + ": " + e.getMessage()); + } + } + + Messages.sendConsole("Rollback to version " + targetVersion + " complete"); + } + + public int getCurrentVersion() { + return CURRENT_VERSION; + } + + public List getMigrations() { + return migrations; + } +} + From 6e0d308d6253e21d10ffbe1065dee2df7c51bd0f Mon Sep 17 00:00:00 2001 From: Tristan Vermeesch Date: Sat, 13 Dec 2025 15:51:01 +0100 Subject: [PATCH 2/2] feat: remove requirement that file name should be the task uuid --- build.gradle | 4 +- java17-build.gradle | 4 +- java21-build.gradle | 4 +- .../playbosswar/com/CommandTimerPlugin.java | 1 - .../com/tasks/AdHocCommandsManager.java | 14 +- .../java/me/playbosswar/com/tasks/Task.java | 45 +++++- .../playbosswar/com/tasks/TasksManager.java | 1 + .../java/me/playbosswar/com/utils/Files.java | 146 ++++++++++++------ .../utils/migrations/MigrationManager.java | 9 ++ src/main/resources/plugin.yml | 2 +- 10 files changed, 168 insertions(+), 62 deletions(-) diff --git a/build.gradle b/build.gradle index 334dcb08..1e94a135 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ java { } group = 'me.playbosswar.com' -version = '8.15.1' +version = '8.16.0' description = 'CommandTimer' repositories { @@ -74,7 +74,7 @@ publishing { maven(MavenPublication) { groupId = 'me.playbosswar.com' artifactId = 'commandtimer' - version = '8.15.1' + version = '8.16.0' from components.java } diff --git a/java17-build.gradle b/java17-build.gradle index 4410299d..4b4190d7 100644 --- a/java17-build.gradle +++ b/java17-build.gradle @@ -10,7 +10,7 @@ java { group = 'me.playbosswar.com' -version = '8.15.1' +version = '8.16.0' description = 'CommandTimer' repositories { @@ -63,7 +63,7 @@ publishing { maven(MavenPublication) { groupId = 'me.playbosswar.com' artifactId = 'commandtimer-java17' - version = '8.15.1' + version = '8.16.0' from components.java } diff --git a/java21-build.gradle b/java21-build.gradle index 09de9db9..f0cebf76 100644 --- a/java21-build.gradle +++ b/java21-build.gradle @@ -10,7 +10,7 @@ java { group = 'me.playbosswar.com' -version = '8.15.1' +version = '8.16.0' description = 'CommandTimer' repositories { @@ -67,7 +67,7 @@ publishing { maven(MavenPublication) { groupId = 'me.playbosswar.com' artifactId = 'commandtimer-java21' - version = '8.15.1' + version = '8.16.0' from components.java } } diff --git a/src/main/java/me/playbosswar/com/CommandTimerPlugin.java b/src/main/java/me/playbosswar/com/CommandTimerPlugin.java index f7a83ebc..f5880857 100644 --- a/src/main/java/me/playbosswar/com/CommandTimerPlugin.java +++ b/src/main/java/me/playbosswar/com/CommandTimerPlugin.java @@ -88,7 +88,6 @@ public void onEnable() { Bukkit.getPluginManager().registerEvents(new JoinEvents(), this); - Files.migrateFileNamesToFileUuids(); new MigrationManager(this).runMigrations(); if(getConfig().getBoolean("database.enabled")) { try { diff --git a/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java b/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java index d9120762..13cd4701 100644 --- a/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java +++ b/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java @@ -123,9 +123,17 @@ private List loadAdHocCommandsFromFiles() { private void storeCommand(AdHocCommand command) { GsonConverter gson = new GsonConverter(); String json = gson.toJson(command); - try (FileWriter jsonFile = new FileWriter(Files.getAdHocCommandFile(command.getId()))) { - jsonFile.write(json); - jsonFile.flush(); + try { + String path; + try { + path = Files.getAdHocCommandFile(command.getId()); + } catch (IllegalStateException e) { + path = Files.getNewAdHocCommandFile(command.getId()); + } + try (FileWriter jsonFile = new FileWriter(path)) { + jsonFile.write(json); + jsonFile.flush(); + } } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/me/playbosswar/com/tasks/Task.java b/src/main/java/me/playbosswar/com/tasks/Task.java index 77d0c83e..8b19f04e 100644 --- a/src/main/java/me/playbosswar/com/tasks/Task.java +++ b/src/main/java/me/playbosswar/com/tasks/Task.java @@ -14,7 +14,12 @@ import me.playbosswar.com.tasks.persistors.*; import me.playbosswar.com.utils.Files; import me.playbosswar.com.utils.gson.GsonConverter; +import me.playbosswar.com.utils.migrations.MigrationManager; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.File; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.sql.SQLException; @@ -342,9 +347,43 @@ public void storeInstance() { String json = gson.toJson(this); transaction.setContext("task", json); - try (FileWriter jsonFile = new FileWriter(Files.getTaskFile(id))) { - jsonFile.write(json); - jsonFile.flush(); + try { + String path; + File existingFile = null; + try { + path = Files.getTaskFile(id); + existingFile = new File(path); + } catch (IllegalStateException e) { + path = Files.getNewTaskFile(id); + } + + JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject(); + + if (existingFile != null && existingFile.exists()) { + try (FileReader fr = new FileReader(existingFile)) { + JsonObject existingJson = new JsonParser().parse(fr).getAsJsonObject(); + if (existingJson.has("configVersion")) { + jsonObject.addProperty("configVersion", existingJson.get("configVersion").getAsInt()); + } else { + jsonObject.addProperty("configVersion", MigrationManager.CURRENT_VERSION); + } + } catch (Exception e) { + if (!jsonObject.has("configVersion")) { + jsonObject.addProperty("configVersion", MigrationManager.CURRENT_VERSION); + } + } + } else { + if (!jsonObject.has("configVersion")) { + jsonObject.addProperty("configVersion", MigrationManager.CURRENT_VERSION); + } + } + + json = gson.toJson(jsonObject); + + try (FileWriter jsonFile = new FileWriter(path)) { + jsonFile.write(json); + jsonFile.flush(); + } } catch (IOException e) { e.printStackTrace(); transaction.setThrowable(e); diff --git a/src/main/java/me/playbosswar/com/tasks/TasksManager.java b/src/main/java/me/playbosswar/com/tasks/TasksManager.java index 2e939ba1..e1ffb3b1 100644 --- a/src/main/java/me/playbosswar/com/tasks/TasksManager.java +++ b/src/main/java/me/playbosswar/com/tasks/TasksManager.java @@ -103,6 +103,7 @@ public void removeTask(Task task) throws IOException { try { loadedTasks.remove(task); java.nio.file.Files.delete(Paths.get(Files.getTaskFile(task.getId()))); + java.nio.file.Files.delete(Paths.get(Files.getTaskLocalExecutionFile(task.getId()))); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/me/playbosswar/com/utils/Files.java b/src/main/java/me/playbosswar/com/utils/Files.java index f0dc1340..a7edd627 100644 --- a/src/main/java/me/playbosswar/com/utils/Files.java +++ b/src/main/java/me/playbosswar/com/utils/Files.java @@ -14,6 +14,8 @@ import org.json.simple.parser.ParseException; import com.google.gson.JsonParseException; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import java.io.File; import java.io.FileReader; @@ -53,14 +55,90 @@ public static void createDataFolders() { CommandTimerPlugin.getPlugin().saveResource("languages/default.json", true); } - /** - * Returns timer json file - */ + private static File findTaskFileByUuid(UUID id) { + File dir = new File(pluginFolderPath + "/timers"); + File[] files = dir.listFiles(file -> file.getName().endsWith(".json")); + if (files == null) return null; + + for (File file : files) { + try (FileReader fr = new FileReader(file)) { + JsonObject json = new JsonParser().parse(fr).getAsJsonObject(); + if (json.has("id")) { + UUID fileId = UUID.fromString(json.get("id").getAsString()); + if (fileId.equals(id)) { + return file; + } + } + } catch (Exception e) { + continue; + } + } + return null; + } + + private static File findMetadataFileByUuid(UUID id) { + File dir = new File(pluginFolderPath + "/execution-data"); + File[] files = dir.listFiles(file -> file.getName().endsWith(".json")); + if (files == null) return null; + + for (File file : files) { + try (FileReader fr = new FileReader(file)) { + JsonObject json = new JsonParser().parse(fr).getAsJsonObject(); + if (json.has("taskId")) { + UUID fileId = UUID.fromString(json.get("taskId").getAsString()); + if (fileId.equals(id)) { + return file; + } + } + } catch (Exception e) { + continue; + } + } + return null; + } + + private static File findAdHocCommandFileByUuid(UUID id) { + File dir = new File(pluginFolderPath + "/ad-hoc-commands"); + File[] files = dir.listFiles(file -> file.getName().endsWith(".json")); + if (files == null) return null; + + for (File file : files) { + try (FileReader fr = new FileReader(file)) { + JsonObject json = new JsonParser().parse(fr).getAsJsonObject(); + if (json.has("id")) { + UUID fileId = UUID.fromString(json.get("id").getAsString()); + if (fileId.equals(id)) { + return file; + } + } + } catch (Exception e) { + continue; + } + } + return null; + } + public static String getTaskFile(UUID id) { - return pluginFolderPath + "/timers/" + id + ".json"; + File file = findTaskFileByUuid(id); + if (file != null) { + return file.getAbsolutePath(); + } + throw new IllegalStateException("Task file not found for UUID: " + id); } public static String getTaskLocalExecutionFile(UUID id) { + File file = findMetadataFileByUuid(id); + if (file != null) { + return file.getAbsolutePath(); + } + throw new IllegalStateException("Task metadata file not found for UUID: " + id); + } + + public static String getNewTaskFile(UUID id) { + return pluginFolderPath + "/timers/" + id + ".json"; + } + + public static String getNewTaskLocalExecutionFile(UUID id) { return pluginFolderPath + "/execution-data/" + id + ".json"; } @@ -69,6 +147,14 @@ public static String getAdHocCommandsDirectory() { } public static String getAdHocCommandFile(UUID id) { + File file = findAdHocCommandFileByUuid(id); + if (file != null) { + return file.getAbsolutePath(); + } + throw new IllegalStateException("Ad-hoc command file not found for UUID: " + id); + } + + public static String getNewAdHocCommandFile(UUID id) { return pluginFolderPath + "/ad-hoc-commands/" + id + ".json"; } @@ -91,53 +177,15 @@ private static void healTask(Task task) { } } - public static void migrateFileNamesToFileUuids() { - File dir = new File(pluginFolderPath + "/timers"); - File[] directoryListing = dir.listFiles(file -> file.getName().endsWith(".json")); - - if(directoryListing != null) { - for(File file : directoryListing) { - if(!file.exists() || !file.getName().contains("json")) { - continue; - } - - try { - UUID.fromString(file.getName().replace(".json", "")); - } catch(IllegalArgumentException e) { - try { - UUID uuid = UUID.randomUUID(); - - FileReader fr = new FileReader(file.getPath()); - JSONParser jsonParser = new JSONParser(); - Task task = new GsonConverter().fromJson(jsonParser.parse(fr).toString(), Task.class); - task.setId(uuid); - - GsonConverter gson = new GsonConverter(); - String json = gson.toJson(task); - try (FileWriter jsonFile = new FileWriter(pluginFolderPath + "/timers/" + uuid + ".json")) { - jsonFile.write(json); - jsonFile.flush(); - } - - file.delete(); - Messages.sendConsole("Migrated " + file.getName() + " to " + uuid + ".json"); - } catch(IOException | ParseException ex) { - throw new RuntimeException(ex); - } - } - } - } - } - public static TaskExecutionMetadata getOrCreateTaskMetadata(Task task) { try { - String path = getTaskLocalExecutionFile(task.getId()); - File file = new File(path); - if (!file.exists()) { + File file = findMetadataFileByUuid(task.getId()); + if (file == null || !file.exists()) { TaskExecutionMetadata metadata = new TaskExecutionMetadata(task.getId(), task.getTimesExecuted(), task.getLastExecutedCommandIndex(), task.getLastExecuted()); GsonConverter gson = new GsonConverter(); String json = gson.toJson(metadata); + String path = getNewTaskLocalExecutionFile(task.getId()); try (FileWriter jsonFile = new FileWriter(path)) { jsonFile.write(json); jsonFile.flush(); @@ -145,7 +193,7 @@ public static TaskExecutionMetadata getOrCreateTaskMetadata(Task task) { return metadata; } - FileReader fr = new FileReader(getTaskLocalExecutionFile(task.getId())); + FileReader fr = new FileReader(file); JSONParser jsonParser = new JSONParser(); TaskExecutionMetadata metadata = new GsonConverter().fromJson(jsonParser.parse(fr).toString(), TaskExecutionMetadata.class); @@ -162,7 +210,9 @@ public static void updateLocalTaskMetadata(Task task) { GsonConverter gson = new GsonConverter(); String json = gson.toJson(metadata); - try (FileWriter jsonFile = new FileWriter(getTaskLocalExecutionFile(task.getId()))) { + File file = findMetadataFileByUuid(task.getId()); + String path = file != null ? file.getAbsolutePath() : getNewTaskLocalExecutionFile(task.getId()); + try (FileWriter jsonFile = new FileWriter(path)) { jsonFile.write(json); jsonFile.flush(); } catch (IOException e) { @@ -181,7 +231,7 @@ public static List deserializeJsonFilesIntoCommandTimers() { try { if(directoryListing != null) { for(File file : directoryListing) { - if(!file.exists() || !file.getName().contains("json")) { + if(!file.exists() || !file.getName().contains(".json")) { continue; } diff --git a/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java b/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java index b140536f..85ebafe9 100644 --- a/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java +++ b/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java @@ -46,6 +46,7 @@ public void runMigrations() { Messages.sendConsole("Migrating task file: " + file.getName() + " (v" + fileVersion + " -> v" + CURRENT_VERSION + ")"); + boolean failed = false; for (Migration migration : migrations.stream() .filter(m -> m.getVersion() > fileVersion) .sorted(Comparator.comparingInt(Migration::getVersion)) @@ -55,9 +56,17 @@ public void runMigrations() { } catch (Exception e) { Messages.sendConsole("Migration v" + migration.getVersion() + " failed for " + file.getName() + ": " + e.getMessage()); e.printStackTrace(); + failed = true; + break; } } + if (failed) { + Messages.sendConsole("Migration failed for " + file.getName() + ". Disabling plugin..."); + CommandTimerPlugin.getInstance().getServer().getPluginManager().disablePlugin(CommandTimerPlugin.getInstance()); + return; + } + Messages.sendConsole("Migration complete for " + file.getName() + " (v" + fileVersion + " -> v" + CURRENT_VERSION + ")"); json.addProperty("configVersion", CURRENT_VERSION); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4c83ba49..7b94e9d0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ main: me.playbosswar.com.CommandTimerPlugin name: "CommandTimer" -version: "8.15.1" +version: "8.16.0" description: "Schedule commands like you want" author: PlayBossWar api-version: 1.13