diff --git a/build.gradle b/build.gradle index 334dcb0..1e94a13 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 4410299..4b4190d 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 09de9db..f0cebf7 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 5f08618..f588085 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; @@ -87,7 +88,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 38a3c50..baee9da 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 abfcae9..6f4682d 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 3cb705c..13cd470 100644 --- a/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java +++ b/src/main/java/me/playbosswar/com/tasks/AdHocCommandsManager.java @@ -124,10 +124,16 @@ 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(); - jsonFile.close(); + 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 1307ec9..8b19f04 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; @@ -40,9 +45,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 +189,7 @@ public int getTimesExecuted() { public void setTimesExecuted(int timesExecuted) { this.timesExecuted = timesExecuted; + storeExecutionMetadata(); } public Date getLastExecuted() { @@ -192,6 +198,7 @@ public Date getLastExecuted() { public void setLastExecuted(Date lastExecuted) { this.lastExecuted = lastExecuted; + storeExecutionMetadata(); } public boolean isActive() { @@ -264,6 +271,7 @@ public int getLastExecutedCommandIndex() { public void setLastExecutedCommandIndex(int lastExecutedCommandIndex) { this.lastExecutedCommandIndex = lastExecutedCommandIndex; + storeExecutionMetadata(); } public Condition getCondition() { @@ -320,6 +328,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 { @@ -336,10 +348,43 @@ public void storeInstance() { transaction.setContext("task", json); try { - FileWriter jsonFile = new FileWriter(Files.getTaskFile(id)); - jsonFile.write(json); - jsonFile.flush(); - } catch(IOException e) { + 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); 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 e443afc..e8d50b5 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 31d43ab..6218611 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 c233fe9..e1ffb3b 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(); } }); @@ -104,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(); } @@ -159,7 +159,6 @@ public void processCommandExecution(Task task, TaskCommand taskCommand) { if (executed) { task.setLastExecuted(new Date()); task.setTimesExecuted(task.getTimesExecuted() + 1); - task.storeInstance(); } }); } @@ -409,8 +408,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 9c960aa..a7edd62 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; @@ -38,9 +40,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"); @@ -51,15 +55,91 @@ 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) { - return pluginFolderPath + "/timers/" + id + ".local.json"; + 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"; } public static String getAdHocCommandsDirectory() { @@ -67,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"; } @@ -89,59 +177,23 @@ 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")); - - 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); - 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()) { - TaskExecutionMetadata metadata = new TaskExecutionMetadata(task.getTimesExecuted(), - task.getLastExecutedCommandIndex(), task.getLastExecuted()); + 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); - FileWriter jsonFile = new FileWriter(path); - jsonFile.write(json); - jsonFile.flush(); + String path = getNewTaskLocalExecutionFile(task.getId()); + try (FileWriter jsonFile = new FileWriter(path)) { + jsonFile.write(json); + jsonFile.flush(); + } 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); @@ -153,16 +205,17 @@ 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())); + 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) { + } catch (IOException e) { e.printStackTrace(); } } @@ -171,19 +224,19 @@ 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<>(); try { if(directoryListing != null) { for(File file : directoryListing) { - if(!file.exists() || !file.getName().contains("json")) { + if(!file.exists() || !file.getName().contains(".json")) { continue; } 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 +255,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 0000000..04d43f1 --- /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 0000000..2e840fb --- /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 0000000..85ebafe --- /dev/null +++ b/src/main/java/me/playbosswar/com/utils/migrations/MigrationManager.java @@ -0,0 +1,147 @@ +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 + ")"); + + boolean failed = false; + 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(); + 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); + 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; + } +} + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4c83ba4..7b94e9d 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