diff --git a/src/main/java/com/bentahsin/configuration/Configuration.java b/src/main/java/com/bentahsin/configuration/Configuration.java index 50192dd..e0b4723 100644 --- a/src/main/java/com/bentahsin/configuration/Configuration.java +++ b/src/main/java/com/bentahsin/configuration/Configuration.java @@ -1,6 +1,9 @@ package com.bentahsin.configuration; +import com.bentahsin.configuration.annotation.Backup; +import com.bentahsin.configuration.annotation.ConfigVersion; import com.bentahsin.configuration.core.ConfigMapper; +import com.bentahsin.configuration.util.BackupHandler; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; @@ -22,11 +25,11 @@ public Configuration(JavaPlugin plugin) { } /** - * Config dosyasını başlatır, yükler ve eksik ayarları tamamlar. - * Bu metod aynı zamanda "Reload" işlemi için de kullanılabilir. + * Initializes, loads, and completes missing settings for the config file. + * This method can also be used for the "Reload" operation (logic-wise). * - * @param configInstance Config sınıfının örneği (örn: new AntiAfkConfig()) - * @param fileName Dosya adı (örn: "config.yml") + * @param configInstance The instance of the config class (e.g., new AntiAfkConfig()) + * @param fileName The file name (e.g., "config.yml") */ public void init(Object configInstance, String fileName) { File file = new File(plugin.getDataFolder(), fileName); @@ -36,34 +39,61 @@ public void init(Object configInstance, String fileName) { } YamlConfiguration yamlConfig = new YamlConfiguration(); + boolean loadFailed = false; + try { yamlConfig.load(file); - } catch (IOException | InvalidConfigurationException e) { - plugin.getLogger().log(Level.SEVERE, "Config dosyası okunamadı veya bozuk: " + fileName, e); + } catch (InvalidConfigurationException e) { + plugin.getLogger().severe("!!! Critical Error !!!"); + plugin.getLogger().severe(fileName + " is broken! Please check the YAML format."); + + handleBackupOnFailure(configInstance, file); + loadFailed = true; + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Config file could not be read or is corrupt: " + fileName, e); return; } - mapper.loadFromConfig(configInstance, yamlConfig); + if (!loadFailed) { + handleBackupOnMigration(configInstance, yamlConfig, file); + } + + if (!loadFailed) { + mapper.loadFromConfig(configInstance, yamlConfig); + } + + if (!loadFailed) { + mapper.handleVersion(configInstance, yamlConfig); + } else if (configInstance.getClass().isAnnotationPresent(ConfigVersion.class)) { + int v = configInstance.getClass().getAnnotation(ConfigVersion.class).value(); + yamlConfig.set("config-version", v); + } + mapper.saveToConfig(configInstance, yamlConfig); try { yamlConfig.save(file); + if (loadFailed) { + plugin.getLogger().warning("Broken file has been backed up and " + fileName + " has been recreated with default settings."); + } } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Config güncellenirken hata oluştu: " + fileName, e); + plugin.getLogger().log(Level.SEVERE, "Error occurred while updating config: " + fileName, e); } } /** - * Yapılandırmayı sadece yeniden yükler (init ile aynı işlevi görür, okunabilirlik için). + * Reloads the configuration. + * Essentially calls init() but also triggers @OnReload methods. */ @SuppressWarnings("unused") public void reload(Object configInstance, String fileName) { init(configInstance, fileName); + mapper.runOnReload(configInstance); } /** - * Mevcut nesne durumunu dosyaya kaydeder. - * Oyun içi komutla ayar değiştirdiğinde bunu çağırabilirsin. + * Saves the current object state to the file. + * Can be called when changing settings via in-game commands. */ @SuppressWarnings("unused") public void save(Object configInstance, String fileName) { @@ -75,7 +105,7 @@ public void save(Object configInstance, String fileName) { yamlConfig.load(file); } } catch (Exception e) { - plugin.getLogger().warning("Mevcut config yüklenemedi, üzerine yazılıyor: " + fileName); + plugin.getLogger().warning("Could not load existing config, overwriting: " + fileName); } mapper.saveToConfig(configInstance, yamlConfig); @@ -83,12 +113,12 @@ public void save(Object configInstance, String fileName) { try { yamlConfig.save(file); } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Config kaydedilemedi: " + fileName, e); + plugin.getLogger().log(Level.SEVERE, "Could not save config: " + fileName, e); } } /** - * Dosya oluşturma mantığı (Private Helper) + * Internal helper logic for file creation. */ private void createFile(File file, String fileName) { try { @@ -100,11 +130,44 @@ private void createFile(File file, String fileName) { plugin.saveResource(fileName, false); } else { if (file.createNewFile()) { - plugin.getLogger().info("Yeni config dosyası oluşturuldu: " + fileName); + plugin.getLogger().info("New config file created: " + fileName); } } } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Config dosyası oluşturulamadı: " + fileName, e); + plugin.getLogger().log(Level.SEVERE, "Could not create config file: " + fileName, e); + } + } + + /** + * Helper: Checks and handles backup on failure (Syntax Error). + */ + private void handleBackupOnFailure(Object instance, File file) { + if (instance.getClass().isAnnotationPresent(Backup.class)) { + Backup backup = instance.getClass().getAnnotation(Backup.class); + if (backup.enabled() && backup.onFailure()) { + plugin.getLogger().info("Backing up broken file..."); + BackupHandler.createBackup(plugin, file, backup.path(), "broken"); + } + } + } + + /** + * Helper: Checks and handles backup on version migration. + */ + private void handleBackupOnMigration(Object instance, YamlConfiguration config, File file) { + if (instance.getClass().isAnnotationPresent(Backup.class) && + instance.getClass().isAnnotationPresent(ConfigVersion.class)) { + + Backup backup = instance.getClass().getAnnotation(Backup.class); + ConfigVersion versionAnno = instance.getClass().getAnnotation(ConfigVersion.class); + + int fileVersion = config.getInt("config-version", 0); + int classVersion = versionAnno.value(); + + if (fileVersion < classVersion && backup.enabled() && backup.onMigration()) { + plugin.getLogger().info("Upgrading version (" + fileVersion + " -> " + classVersion + "). Backing up..."); + BackupHandler.createBackup(plugin, file, backup.path(), "v" + fileVersion); + } } } } \ No newline at end of file diff --git a/src/main/java/com/bentahsin/configuration/annotation/Backup.java b/src/main/java/com/bentahsin/configuration/annotation/Backup.java new file mode 100644 index 0000000..b2c1dc9 --- /dev/null +++ b/src/main/java/com/bentahsin/configuration/annotation/Backup.java @@ -0,0 +1,33 @@ +package com.bentahsin.configuration.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Backup { + /** + * Enables or disables the backup system. + */ + boolean enabled() default true; + + /** + * The folder path where backups will be stored. + * Specifies the location inside the plugin folder. + */ + String path() default "backups"; + + /** + * If a YAML error (syntax error) occurs while loading the config, + * should a backup of the corrupted file be taken? + */ + boolean onFailure() default true; + + /** + * When the config version changes (migration), + * should a backup of the old config file be taken? + */ + boolean onMigration() default true; +} \ No newline at end of file diff --git a/src/main/java/com/bentahsin/configuration/annotation/ConfigVersion.java b/src/main/java/com/bentahsin/configuration/annotation/ConfigVersion.java new file mode 100644 index 0000000..ec533de --- /dev/null +++ b/src/main/java/com/bentahsin/configuration/annotation/ConfigVersion.java @@ -0,0 +1,17 @@ +package com.bentahsin.configuration.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies the version of the config file. + * If the "config-version" value in the file is lower than this value, + * the library adds missing settings to the file and updates the version. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ConfigVersion { + int value() default 1; +} \ No newline at end of file diff --git a/src/main/java/com/bentahsin/configuration/annotation/OnReload.java b/src/main/java/com/bentahsin/configuration/annotation/OnReload.java new file mode 100644 index 0000000..fd43a5c --- /dev/null +++ b/src/main/java/com/bentahsin/configuration/annotation/OnReload.java @@ -0,0 +1,14 @@ +package com.bentahsin.configuration.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks methods to be executed when the config is reloaded. + * Example: To refresh database connections or clear caches. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnReload {} \ No newline at end of file diff --git a/src/main/java/com/bentahsin/configuration/converter/impl/ItemStackConverter.java b/src/main/java/com/bentahsin/configuration/converter/impl/ItemStackConverter.java index 41f0aec..19375ed 100644 --- a/src/main/java/com/bentahsin/configuration/converter/impl/ItemStackConverter.java +++ b/src/main/java/com/bentahsin/configuration/converter/impl/ItemStackConverter.java @@ -81,7 +81,7 @@ public ItemStack convertToField(Map source) { return item; } catch (Exception e) { - Bukkit.getLogger().log(Level.WARNING, "[Configuration] ItemStack dönüştürülürken hata oluştu!", e); + Bukkit.getLogger().log(Level.WARNING, "[Configuration] Encountered an error while transforming ItemStack!", e); return new ItemStack(Material.AIR); } } diff --git a/src/main/java/com/bentahsin/configuration/core/ConfigMapper.java b/src/main/java/com/bentahsin/configuration/core/ConfigMapper.java index 2050ab3..e3bdba9 100644 --- a/src/main/java/com/bentahsin/configuration/core/ConfigMapper.java +++ b/src/main/java/com/bentahsin/configuration/core/ConfigMapper.java @@ -18,6 +18,35 @@ public ConfigMapper(Logger logger) { this.logger = logger; } + public void handleVersion(Object instance, ConfigurationSection config) { + if (!instance.getClass().isAnnotationPresent(ConfigVersion.class)) return; + + int classVersion = instance.getClass().getAnnotation(ConfigVersion.class).value(); + int fileVersion = config.getInt("config-version", 0); + + if (fileVersion < classVersion) { + logger.info("Updating config version: v" + fileVersion + " -> v" + classVersion); + config.set("config-version", classVersion); + } + } + + public void runOnReload(Object instance) { + for (Method method : instance.getClass().getDeclaredMethods()) { + if (method.isAnnotationPresent(OnReload.class)) { + if (!trySetAccessible(method)) continue; + + try { + method.setAccessible(true); + logger.info("Running reload trigger: " + method.getName()); + method.invoke(instance); + } catch (Exception e) { + logger.severe("Error running OnReload method: " + method.getName()); + logger.severe(e.getMessage()); + } + } + } + } + public void loadFromConfig(Object instance, ConfigurationSection config) { if (instance == null || config == null) return; processClass(instance, config); @@ -38,7 +67,7 @@ private void processClass(Object instance, ConfigurationSection config) { String pathKey = getPathKey(field); try { - field.setAccessible(true); + if (!trySetAccessible(field)) continue; if (field.isAnnotationPresent(Transform.class)) { Object val = config.get(pathKey); @@ -61,8 +90,13 @@ private void processClass(Object instance, ConfigurationSection config) { if (isComplexObject(field.getType())) { Object fieldInstance = field.get(instance); if (fieldInstance == null) { - fieldInstance = field.getType().getDeclaredConstructor().newInstance(); - field.set(instance, fieldInstance); + Constructor constructor = field.getType().getDeclaredConstructor(); + if (trySetAccessible(constructor)) { + fieldInstance = constructor.newInstance(); + field.set(instance, fieldInstance); + } else { + continue; + } } ConfigurationSection subSection = config.getConfigurationSection(pathKey); @@ -82,7 +116,7 @@ private void processClass(Object instance, ConfigurationSection config) { } } catch (Exception e) { - logger.warning("Config yüklenirken hata (" + field.getName() + "): " + e.getMessage()); + logger.warning("Error loading config (" + field.getName() + "): " + e.getMessage()); } } } @@ -105,7 +139,7 @@ private void saveClass(Object instance, ConfigurationSection config) { String path = getPathKey(field); try { - field.setAccessible(true); + if (!trySetAccessible(field)) continue; Object value = field.get(instance); if (value == null) continue; @@ -144,7 +178,7 @@ private void saveClass(Object instance, ConfigurationSection config) { config.set(path, value); } catch (Exception e) { - logger.severe("Kaydetme hatası: " + e.getMessage()); + logger.severe("Save error: " + e.getMessage()); } } } @@ -200,7 +234,7 @@ private void handleListSave(Field field, ConfigurationSection config, String pat Map objectMap = new LinkedHashMap<>(); for (Field objField : obj.getClass().getDeclaredFields()) { if (!shouldProcess(objField)) continue; - objField.setAccessible(true); + if (!trySetAccessible(objField)) continue; String objPath = getPathKey(objField); objectMap.put(objPath, objField.get(obj)); } @@ -282,7 +316,7 @@ private Object convertKey(String key, Class targetType) { if (targetType == Double.class || targetType == double.class) return Double.parseDouble(key); if (targetType == UUID.class) return UUID.fromString(key); } catch (Exception e) { - logger.warning("Map Key dönüştürülemedi: " + key + " -> " + targetType.getSimpleName()); + logger.warning("Map Key conversion failed: " + key + " -> " + targetType.getSimpleName()); } return null; } @@ -290,12 +324,12 @@ private Object convertKey(String key, Class targetType) { private Object createInstance(Class clazz) throws Exception { try { Constructor constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); + trySetAccessible(constructor); return constructor.newInstance(); } catch (NoSuchMethodException e) { if (clazz.getEnclosingClass() != null && !Modifier.isStatic(clazz.getModifiers())) { - throw new IllegalStateException("HATA: '" + clazz.getSimpleName() + "' sınıfı bir Inner Class ve STATIC değil! " + - "Lütfen config sınıflarını 'public static class' olarak tanımlayın."); + throw new IllegalStateException("ERROR: '" + clazz.getSimpleName() + "' is an Inner Class and NOT STATIC! " + + "Please define config classes as 'public static class'."); } throw e; } @@ -393,7 +427,7 @@ private void safeSetField(Object instance, Field field, Object value) throws Ill if (value == null) { if (validate.notNull()) { - logger.warning("Config Hatası: " + field.getName() + " null olamaz!"); + logger.warning("Config Error: " + field.getName() + " cannot be null!"); } return; } @@ -401,7 +435,7 @@ private void safeSetField(Object instance, Field field, Object value) throws Ill if (value instanceof Number) { double val = ((Number) value).doubleValue(); if (val < validate.min() || val > validate.max()) { - logger.warning(String.format("Config Sınır Hatası (%s): %s (Min:%s Max:%s)", field.getName(), val, validate.min(), validate.max())); + logger.warning(String.format("Config Limit Error (%s): %s (Min:%s Max:%s)", field.getName(), val, validate.min(), validate.max())); return; } } @@ -409,8 +443,8 @@ private void safeSetField(Object instance, Field field, Object value) throws Ill if (value instanceof String && !validate.pattern().isEmpty()) { String strVal = (String) value; if (!strVal.matches(validate.pattern())) { - logger.warning("Config Format Hatası (" + field.getName() + "): Değer '" + strVal + "' formata uymuyor."); - logger.warning("Beklenen Regex: " + validate.pattern()); + logger.warning("Config Format Error (" + field.getName() + "): Value '" + strVal + "' does not match format."); + logger.warning("Expected Regex: " + validate.pattern()); return; } } @@ -425,7 +459,7 @@ private void safeSetField(Object instance, Field field, Object value) throws Ill Enum enumValue = Enum.valueOf((Class) type, ((String) value).toUpperCase(Locale.ENGLISH)); field.set(instance, enumValue); } catch (IllegalArgumentException e) { - logger.warning("Enum Hatası: '" + field.getName() + "' geçersiz: " + value); + logger.warning("Enum Error: '" + field.getName() + "' invalid: " + value); } } return; @@ -451,7 +485,7 @@ private void safeSetField(Object instance, Field field, Object value) throws Ill if (type.isAssignableFrom(value.getClass())) { field.set(instance, value); } else { - logger.warning("Tip Uyuşmazlığı: '" + field.getName() + "' Beklenen: " + type.getSimpleName() + ", Gelen: " + value.getClass().getSimpleName()); + logger.warning("Type Mismatch: '" + field.getName() + "' Expected: " + type.getSimpleName() + ", Got: " + value.getClass().getSimpleName()); } } @@ -472,8 +506,9 @@ private String getPathKey(Field field) { private void setComments(ConfigurationSection config, String path, List comments) { try { Method method = config.getClass().getMethod("setComments", String.class, List.class); - method.setAccessible(true); - method.invoke(config, path, comments); + if (trySetAccessible(method)) { + method.invoke(config, path, comments); + } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {} } @@ -481,7 +516,7 @@ private void runPostLoad(Object instance) { for (Method method : instance.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(PostLoad.class)) { try { - method.setAccessible(true); + if (!trySetAccessible(method)) continue; method.invoke(instance); } catch (Exception e) { logger.warning("PostLoad metodu çalışırken hata: " + method.getName()); @@ -490,4 +525,20 @@ private void runPostLoad(Object instance) { } } } + + /** + * AccessibleObject (Field, Method, Constructor) üzerinde setAccessible(true) yapmayı dener. + * + * @param object Erişilmek istenen Field, Method veya Constructor. + * @return Erişim başarılıysa true, SecurityException alınırsa false. + */ + private boolean trySetAccessible(AccessibleObject object) { + try { + object.setAccessible(true); + return true; + } catch (Exception e) { + logger.severe("Cannot access member " + object.toString() + " due to security restrictions: " + e.getMessage()); + return false; + } + } } \ No newline at end of file diff --git a/src/main/java/com/bentahsin/configuration/util/BackupHandler.java b/src/main/java/com/bentahsin/configuration/util/BackupHandler.java new file mode 100644 index 0000000..7440654 --- /dev/null +++ b/src/main/java/com/bentahsin/configuration/util/BackupHandler.java @@ -0,0 +1,51 @@ +package com.bentahsin.configuration.util; + +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Level; + +/** + * Utility class for creating timestamped backups of configuration files. + *

+ * Usage: Call {@link #createBackup(JavaPlugin, File, String, String)} to create a backup of a configuration file. + */ +public class BackupHandler { + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + + /** + * Creates a backup of the specified file in the target directory. + * + * @param plugin JavaPlugin instance (for Logging and Path resolution) + * @param sourceFile The source file to back up (e.g. config.yml) + * @param folderPath The backup directory name (relative to plugin data folder, e.g. "backups") + * @param suffix Tag to append to the filename (e.g. "migration", "broken") + */ + public static void createBackup(JavaPlugin plugin, File sourceFile, String folderPath, String suffix) { + if (!sourceFile.exists()) return; + + File backupFolder = new File(plugin.getDataFolder(), folderPath); + if (!backupFolder.exists()) { + boolean ignored = backupFolder.mkdirs(); + } + + String fileName = sourceFile.getName().replace(".yml", ""); + String timestamp = DATE_FORMAT.format(new Date()); + String backupName = String.format("%s_%s_%s.yml", fileName, suffix, timestamp); + + File backupFile = new File(backupFolder, backupName); + + try { + Files.copy(sourceFile.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + plugin.getLogger().info("Backup created: " + backupFile.getPath()); + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Error occurred during backup creation!", e); + } + } +} \ No newline at end of file