diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b208119..f71f8991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ API: Internal: * The config key pattern is cached now. +* Major refactoring related to how the config and language files are loaded. Migration notes: * The folder structure has changed: diff --git a/src/main/java/com/nisovin/shopkeepers/Messages.java b/src/main/java/com/nisovin/shopkeepers/Messages.java index 90994a33..1a2f5302 100644 --- a/src/main/java/com/nisovin/shopkeepers/Messages.java +++ b/src/main/java/com/nisovin/shopkeepers/Messages.java @@ -1,27 +1,28 @@ package com.nisovin.shopkeepers; -import static com.nisovin.shopkeepers.config.ConfigHelper.loadConfigValue; -import static com.nisovin.shopkeepers.config.ConfigHelper.toConfigKey; - import java.io.File; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; +import com.nisovin.shopkeepers.config.Config; import com.nisovin.shopkeepers.config.ConfigLoadException; +import com.nisovin.shopkeepers.config.annotation.WithDefaultValueType; +import com.nisovin.shopkeepers.config.annotation.WithValueTypeProvider; +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.types.ColoredStringListValue; +import com.nisovin.shopkeepers.config.value.types.ColoredStringValue; import com.nisovin.shopkeepers.text.Text; import com.nisovin.shopkeepers.util.Log; import com.nisovin.shopkeepers.util.TextUtils; -public class Messages { +@WithDefaultValueType(fieldType = String.class, valueType = ColoredStringValue.class) +@WithValueTypeProvider(ColoredStringListValue.Provider.class) +public class Messages extends Config { // TODO Replace all with Text? Will require converting back to String, especially for texts used by items. public static String shopTypeAdminRegular = c("Admin shop"); @@ -360,65 +361,80 @@ public class Messages { plugin.saveResource(languageFilePath, false); } - // Load language config: + // Load messages from language config: if (!languageFile.exists()) { Log.warning("Could not find language file '" + languageFile.getName() + "'!"); } else { Log.info("Loading language file: " + languageFile.getName()); try { - YamlConfiguration langConfig = new YamlConfiguration(); - langConfig.load(languageFile); - loadLanguageConfiguration(langConfig); + // Load language config: + YamlConfiguration languageConfig = new YamlConfiguration(); + languageConfig.load(languageFile); + + // Load messages: + INSTANCE.load(languageConfig); + + // Also update the derived settings: + Settings.onSettingsChanged(); } catch (Exception e) { Log.warning("Could not load language file '" + languageFile.getName() + "'!", e); } } } - private static void loadLanguageConfiguration(Configuration config) throws ConfigLoadException { - Set messageKeys = new HashSet<>(); - try { - Field[] fields = Messages.class.getDeclaredFields(); - for (Field field : fields) { - if (field.isSynthetic()) continue; - if (!Modifier.isPublic(field.getModifiers())) { - continue; - } - Class typeClass = field.getType(); - Class genericType = null; - if (typeClass == List.class) { - genericType = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; - } - String configKey = toConfigKey(field.getName()); - messageKeys.add(configKey); - if (!config.isSet(configKey)) { - Log.warning(" Missing message: " + configKey); - continue; // Skip, keeps current value (default) - } + ///// - Object value = loadConfigValue(config, configKey, Collections.emptySet(), typeClass, genericType); - if (value == null) { - Log.warning(" Could not load message: " + configKey); - continue; // Skip, keeps current value (default) - } - field.set(null, value); - } - } catch (Exception e) { - throw new ConfigLoadException("Error while loading messages from language file!", e); - } - - Set configKeys = config.getKeys(false); - if (configKeys.size() != messageKeys.size()) { - for (String configKey : configKeys) { - if (!messageKeys.contains(configKey)) { - Log.warning(" Unknown message: " + configKey); - } - } - } - - Settings.onSettingsChanged(); - } + private static final Messages INSTANCE = new Messages(); private Messages() { } + + @Override + protected String getLogPrefix() { + return "Language: "; + } + + @Override + protected String msgMissingValue(String configKey) { + return this.getLogPrefix() + "Missing message: " + configKey; + } + + @Override + protected String msgUsingDefaultForMissingValue(String configKey, Object defaultValue) { + return this.getLogPrefix() + "Using default value for missing message: " + configKey; + } + + @Override + protected String msgSettingLoadException(String configKey, SettingLoadException e) { + return this.getLogPrefix() + "Could not load message '" + configKey + "': " + e.getMessage(); + } + + @Override + protected String msgDefaultSettingLoadException(String configKey, SettingLoadException e) { + return this.getLogPrefix() + "Could not load default value for message '" + configKey + "': " + e.getMessage(); + } + + @Override + protected String msgInsertingDefault(String configKey) { + return this.getLogPrefix() + "Inserting default value for missing message: " + configKey; + } + + @Override + protected String msgMissingDefault(String configKey) { + return this.getLogPrefix() + "Missing default value for message: " + configKey; + } + + @Override + protected void validateConfig(ConfigurationSection config) throws ConfigLoadException { + super.validateConfig(config); + + // Check for unexpected (possibly no longer existing) message keys: + Set messageKeys = this.getSettings().stream().map(this::getConfigKey).collect(Collectors.toSet()); + Set configKeys = config.getKeys(false); + for (String configKey : configKeys) { + if (!messageKeys.contains(configKey)) { + Log.warning(this.getLogPrefix() + "Unknown message: " + configKey); + } + } + } } diff --git a/src/main/java/com/nisovin/shopkeepers/Settings.java b/src/main/java/com/nisovin/shopkeepers/Settings.java index eebec6c8..4e578f4a 100644 --- a/src/main/java/com/nisovin/shopkeepers/Settings.java +++ b/src/main/java/com/nisovin/shopkeepers/Settings.java @@ -1,16 +1,8 @@ package com.nisovin.shopkeepers; -import static com.nisovin.shopkeepers.config.ConfigHelper.loadConfigValue; -import static com.nisovin.shopkeepers.config.ConfigHelper.setConfigValue; -import static com.nisovin.shopkeepers.config.ConfigHelper.toConfigKey; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -26,6 +18,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import com.nisovin.shopkeepers.api.ShopkeepersPlugin; +import com.nisovin.shopkeepers.config.Config; import com.nisovin.shopkeepers.config.ConfigLoadException; import com.nisovin.shopkeepers.config.migration.ConfigMigrations; import com.nisovin.shopkeepers.util.ConversionUtils; @@ -34,9 +27,8 @@ import com.nisovin.shopkeepers.util.ItemData; import com.nisovin.shopkeepers.util.ItemUtils; import com.nisovin.shopkeepers.util.Log; import com.nisovin.shopkeepers.util.PermissionUtils; -import com.nisovin.shopkeepers.util.Utils; -public class Settings { +public class Settings extends Config { /* * General Settings @@ -330,7 +322,7 @@ public class Settings { setup(); } - // Gets called after the config has been loaded: + // Gets called after setting values have changed (eg. after the config has been loaded): private static void setup() { // Ignore display name (which is used for specifying the new shopkeeper name): namingItemData = new ItemData(ItemUtils.setItemStackName(nameItem.createItemStack(), null)); @@ -349,7 +341,7 @@ public class Settings { try { shopNamePattern = Pattern.compile("^" + Settings.nameRegex + "$"); } catch (PatternSyntaxException e) { - Log.warning("Config: 'name-regex' is not a valid regular expression ('" + Settings.nameRegex + "'). Reverting to default."); + Log.warning(INSTANCE.getLogPrefix() + "'name-regex' is not a valid regular expression ('" + Settings.nameRegex + "'). Reverting to default."); Settings.nameRegex = "[A-Za-z0-9 ]{3,32}"; shopNamePattern = Pattern.compile("^" + Settings.nameRegex + "$"); } @@ -363,7 +355,7 @@ public class Settings { // Validate: Integer maxShops = ConversionUtils.parseInt(permOption); if (maxShops == null || maxShops <= 0) { - Log.warning("Config: Ignoring invalid entry in 'max-shops-perm-options': " + permOption); + Log.warning(INSTANCE.getLogPrefix() + "Ignoring invalid entry in 'max-shops-perm-options': " + permOption); continue; } String permission = "shopkeeper.maxshops." + permOption; @@ -380,16 +372,16 @@ public class Settings { foundInvalidEntityType = true; if ("PIG_ZOMBIE".equals(entityTypeId)) { // Migration note for MC 1.16 TODO Remove this again at some point? - Log.warning("Config: Ignoring mob type 'PIG_ZOMBIE' in setting 'enabled-living-shops'. This mob no longer exist since MC 1.16. Consider replacing it with 'ZOMBIFIED_PIGLIN'."); + Log.warning(INSTANCE.getLogPrefix() + "Ignoring mob type 'PIG_ZOMBIE' in setting 'enabled-living-shops'. This mob no longer exist since MC 1.16. Consider replacing it with 'ZOMBIFIED_PIGLIN'."); } else { - Log.warning("Config: Invalid living entity type name in 'enabled-living-shops': " + entityTypeId); + Log.warning(INSTANCE.getLogPrefix() + "Invalid living entity type name in 'enabled-living-shops': " + entityTypeId); } } else { enabledLivingShops.add(entityType); } } if (foundInvalidEntityType) { - Log.warning("Config: All existing entity type names can be found here: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html"); + Log.warning(INSTANCE.getLogPrefix() + "All existing entity type names can be found here: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html"); } } @@ -549,7 +541,7 @@ public class Settings { public static ConfigLoadException loadConfig(Plugin plugin) { Log.info("Loading config."); - // Save default config in case the config file doesn't exist: + // Save default config in case the config file does not exist: plugin.saveDefaultConfig(); // Load config: @@ -559,7 +551,7 @@ public class Settings { // Load settings from config: boolean configChanged = false; try { - configChanged = Settings.loadConfiguration(config); + configChanged = loadConfig(config); } catch (ConfigLoadException e) { // Config loading failed with a severe issue: return e; @@ -573,14 +565,8 @@ public class Settings { return null; // Config loaded successfully } - // These String / String list settings are exempt from color conversion: - private static final Set noColorConversionKeys = new HashSet<>(Arrays.asList( - toConfigKey("debugOptions"), toConfigKey("fileEncoding"), toConfigKey("shopCreationItemSpawnEggEntityType"), - toConfigKey("maxShopsPermOptions"), toConfigKey("enabledLivingShops"), toConfigKey("nameRegex"), - toConfigKey("language"))); - - // Returns true, if the config misses values which need to be saved - public static boolean loadConfiguration(Configuration config) throws ConfigLoadException { + // Returns true, if the config has changed and needs to be saved. + private static boolean loadConfig(Configuration config) throws ConfigLoadException { boolean configChanged = false; // Perform config migrations: @@ -589,104 +575,68 @@ public class Settings { configChanged = true; } - try { - Field[] fields = Settings.class.getDeclaredFields(); - for (Field field : fields) { - if (field.isSynthetic()) continue; - if (!Modifier.isPublic(field.getModifiers())) { - continue; - } - Class typeClass = field.getType(); - Class genericType = null; - if (typeClass == List.class) { - genericType = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; - } - String configKey = toConfigKey(field.getName()); - - // Initialize the setting with the default value, if it is missing in the config - if (!config.isSet(configKey)) { - Log.warning("Config: Inserting default value for missing config entry: " + configKey); - - // Determine default value: - Configuration defaults = config.getDefaults(); - Object defaultValue = loadConfigValue(defaults, configKey, noColorConversionKeys, typeClass, genericType); - - // Validate default value: - if (defaultValue == null) { - Log.warning("Config: Missing default value for missing config entry: " + configKey); - continue; - } else if (!Utils.isAssignableFrom(typeClass, defaultValue.getClass())) { - Log.warning("Config: Default value for missing config entry '" + configKey + "' is of wrong type: " - + "Got " + defaultValue.getClass().getName() + ", expecting " + typeClass.getName()); - continue; - } - - // Set default value: - setConfigValue(config, configKey, noColorConversionKeys, typeClass, genericType, defaultValue); - configChanged = true; - } - - // Load value: - Object value = loadConfigValue(config, configKey, noColorConversionKeys, typeClass, genericType); - field.set(null, value); - } - } catch (Exception e) { - throw new ConfigLoadException("Error while loading config values!", e); + // Insert default values for settings missing inside the config: + boolean insertedDefaults = Settings.INSTANCE.insertMissingDefaultValues(config); + if (insertedDefaults) { + configChanged = true; } - // Validation: - validateSettings(); + // Load and validate settings: + Settings.INSTANCE.load(config); onSettingsChanged(); return configChanged; } - private static void validateSettings() { + ///// + + private static final Settings INSTANCE = new Settings(); + + private Settings() { + } + + @Override + protected void validateSettings() { if (maxContainerDistance > 50) { - Log.warning("Config: 'max-container-distance' can be at most 50."); + Log.warning(this.getLogPrefix() + "'max-container-distance' can be at most 50."); maxContainerDistance = 50; } if (gravityChunkRange < 0) { - Log.warning("Config: 'gravity-chunk-range' cannot be negative."); + Log.warning(this.getLogPrefix() + "'gravity-chunk-range' cannot be negative."); gravityChunkRange = 0; } // Certain items cannot be of type AIR: if (shopCreationItem.getType() == Material.AIR) { - Log.warning("Config: 'shop-creation-item' can not be AIR."); + Log.warning(this.getLogPrefix() + "'shop-creation-item' can not be AIR."); shopCreationItem = shopCreationItem.withType(Material.VILLAGER_SPAWN_EGG); } if (hireItem.getType() == Material.AIR) { - Log.warning("Config: 'hire-item' can not be AIR."); + Log.warning(this.getLogPrefix() + "'hire-item' can not be AIR."); hireItem = hireItem.withType(Material.EMERALD); } if (currencyItem.getType() == Material.AIR) { - Log.warning("Config: 'currency-item' can not be AIR."); + Log.warning(this.getLogPrefix() + "'currency-item' can not be AIR."); currencyItem = currencyItem.withType(Material.EMERALD); } if (namingOfPlayerShopsViaItem) { if (nameItem.getType() == Material.AIR) { - Log.warning("Config: 'name-item' can not be AIR if naming-of-player-shops-via-item is enabled!"); + Log.warning(this.getLogPrefix() + "'name-item' can not be AIR if naming-of-player-shops-via-item is enabled!"); nameItem = nameItem.withType(Material.NAME_TAG); } } if (maxTradesPages < 1) { - Log.warning("Config: 'max-trades-pages' can not be less than 1!"); + Log.warning(this.getLogPrefix() + "'max-trades-pages' can not be less than 1!"); maxTradesPages = 1; } else if (maxTradesPages > 10) { - Log.warning("Config: 'max-trades-pages' can not be greater than 10!"); + Log.warning(this.getLogPrefix() + "'max-trades-pages' can not be greater than 10!"); maxTradesPages = 10; } if (taxRate < 0) { - Log.warning("Config: 'tax-rate' can not be less than 0!"); + Log.warning(this.getLogPrefix() + "'tax-rate' can not be less than 0!"); taxRate = 0; } else if (taxRate > 100) { - Log.warning("Config: 'tax-rate' can not be larger than 100!"); + Log.warning(this.getLogPrefix() + "'tax-rate' can not be larger than 100!"); taxRate = 100; } } - - ///// - - private Settings() { - } } diff --git a/src/main/java/com/nisovin/shopkeepers/config/Config.java b/src/main/java/com/nisovin/shopkeepers/config/Config.java new file mode 100644 index 00000000..e48c313f --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/Config.java @@ -0,0 +1,485 @@ +package com.nisovin.shopkeepers.config; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; + +import com.nisovin.shopkeepers.config.annotation.Colored; +import com.nisovin.shopkeepers.config.annotation.Uncolored; +import com.nisovin.shopkeepers.config.annotation.WithDefaultValueType; +import com.nisovin.shopkeepers.config.annotation.WithValueType; +import com.nisovin.shopkeepers.config.annotation.WithValueTypeProvider; +import com.nisovin.shopkeepers.config.value.DefaultValueTypes; +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.ValueTypeProvider; +import com.nisovin.shopkeepers.config.value.ValueTypeRegistry; +import com.nisovin.shopkeepers.config.value.types.ColoredStringListValue; +import com.nisovin.shopkeepers.config.value.types.ColoredStringValue; +import com.nisovin.shopkeepers.config.value.types.StringListValue; +import com.nisovin.shopkeepers.config.value.types.StringValue; +import com.nisovin.shopkeepers.util.Log; +import com.nisovin.shopkeepers.util.Utils; +import com.nisovin.shopkeepers.util.Validate; + +public abstract class Config { + + // Custom default value types specified by annotations: + // This is lazily setup if required during settings setup. + private ValueTypeRegistry customDefaultValueTypes = null; + + // Lazily setup cache of all settings and value types: + private Map> valueTypes = null; + private Collection settings = null; + + protected Config() { + } + + protected String getLogPrefix() { + return "Config: "; + } + + // SETTINGS SETUP + + private void setupSettings() { + if (settings != null) { + return; // Already setup + } + assert valueTypes == null; + + this.valueTypes = new LinkedHashMap<>(); + this.settings = Collections.unmodifiableSet(valueTypes.keySet()); + for (Field field : Utils.toIterable(this.streamSettings())) { + ValueType valueType = this.setupValueType(field); + assert valueType != null; + valueTypes.put(field, valueType); + } + + // The custom default value types are only required during the setup: + customDefaultValueTypes = null; + } + + private Stream streamSettings() { + Class configClass = this.getClass(); + Stream settings = this.streamSettings(configClass); + + // Append settings of parent config classes (allows for composition of config classes): + // We stop once we reach this class in the type hierarchy: + Class parentClass = configClass.getSuperclass(); + assert parentClass != null; + while (parentClass != Config.class) { + settings = Stream.concat(settings, this.streamSettings(parentClass)); + parentClass = configClass.getSuperclass(); + assert parentClass != null; + } + return settings; + } + + private final Stream streamSettings(Class configClass) { + List fields = Arrays.asList(configClass.getDeclaredFields()); + return fields.stream().filter(field -> { + // Filter fields: + if (field.isSynthetic()) return false; + if (Modifier.isFinal(field.getModifiers())) return false; + if (!this.isSetting(field)) return false; + return true; + }); + } + + /** + * This can be used to exclude fields from the settings. + *

+ * By default, all public fields are included. Fields can also be declared in parent config classes. + *

+ * Synthetic and final fields are always excluded. + * + * @param field + * the field + * @return true if the field is a setting, false otherwise + */ + protected boolean isSetting(Field field) { + if (!Modifier.isPublic(field.getModifiers())) { + return false; + } + return true; + } + + // Assert: Does not return null. + protected ValueType setupValueType(Field field) { + ValueType valueType = this.getValueTypeByAnnotation(field); + if (valueType != null) return valueType; + + valueType = this.getValueTypeByColoredAnnotation(field); + if (valueType != null) return valueType; + + valueType = this.getValueTypeByUncoloredAnnotation(field); + if (valueType != null) return valueType; + + valueType = this.getValueTypeByCustomDefaults(field); + if (valueType != null) return valueType; + + Type fieldType = field.getGenericType(); + valueType = DefaultValueTypes.get(fieldType); + if (valueType != null) return valueType; + + // ValueType could not be determined: + String configKey = this.getConfigKey(field); + throw new IllegalStateException("Setting '" + configKey + "' is of unsupported type: " + fieldType.getTypeName()); + } + + @SuppressWarnings("unchecked") + protected final ValueType getValueTypeByAnnotation(Field field) { + WithValueType valueTypeAnnotation = field.getAnnotation(WithValueType.class); + if (valueTypeAnnotation != null) { + Class> valueTypeClass = valueTypeAnnotation.value(); + assert valueTypeClass != null; + return (ValueType) instantiateValueType(valueTypeClass); + } + return null; + } + + private static ValueType instantiateValueType(Class> valueTypeClass) { + assert valueTypeClass != null; + try { + return valueTypeClass.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate ValueType: " + valueTypeClass.getName(), e); + } + } + + @SuppressWarnings("unchecked") + protected final ValueType getValueTypeByColoredAnnotation(Field field) { + Colored coloredAnnotation = field.getAnnotation(Colored.class); + if (coloredAnnotation != null) { + Type fieldType = field.getGenericType(); + if (fieldType == String.class) { + return (ValueType) ColoredStringValue.INSTANCE; + } else if (ColoredStringListValue.TYPE_PATTERN.matches(fieldType)) { + return (ValueType) ColoredStringListValue.INSTANCE; + } else { + throw new IllegalArgumentException("The Colored annotation is not supported for settings of type " + fieldType.getTypeName()); + } + } + return null; + } + + @SuppressWarnings("unchecked") + protected final ValueType getValueTypeByUncoloredAnnotation(Field field) { + Uncolored uncoloredAnnotation = field.getAnnotation(Uncolored.class); + if (uncoloredAnnotation != null) { + Type fieldType = field.getGenericType(); + if (fieldType == String.class) { + return (ValueType) StringValue.INSTANCE; + } else if (ColoredStringListValue.TYPE_PATTERN.matches(fieldType)) { + return (ValueType) StringListValue.INSTANCE; + } else { + throw new IllegalArgumentException("The Uncolored annotation is not supported for settings of type " + fieldType.getTypeName()); + } + } + return null; + } + + protected final ValueType getValueTypeByCustomDefaults(Field field) { + this.setupCustomDefaultValueTypes(); // Lazy setup + assert customDefaultValueTypes != null; + Type fieldType = field.getGenericType(); + return customDefaultValueTypes.getValueType(fieldType); + } + + private void setupCustomDefaultValueTypes() { + if (this.customDefaultValueTypes != null) { + return; // Already setup. + } + this.customDefaultValueTypes = new ValueTypeRegistry(); + Class configClass = this.getClass(); + this.setupCustomDefaultValueTypes(configClass); + + // Also take into account custom default value types specified in parent classes: + Class parentClass = configClass.getSuperclass(); + assert parentClass != null; + while (parentClass != Config.class) { + this.setupCustomDefaultValueTypes(parentClass); + parentClass = configClass.getSuperclass(); + assert parentClass != null; + } + } + + private void setupCustomDefaultValueTypes(Class configClass) { + assert customDefaultValueTypes != null; + // WithDefaultValueType annotations: + WithDefaultValueType[] defaultValueTypeAnnotations = configClass.getAnnotationsByType(WithDefaultValueType.class); + assert defaultValueTypeAnnotations != null; + for (WithDefaultValueType defaultValueTypeAnnotation : defaultValueTypeAnnotations) { + Class fieldType = defaultValueTypeAnnotation.fieldType(); + if (customDefaultValueTypes.hasCachedValueType(fieldType)) { + // Only the first encountered default value type specification is used: + continue; + } + + Class> valueTypeClass = defaultValueTypeAnnotation.valueType(); + assert valueTypeClass != null; + ValueType valueType = instantiateValueType(valueTypeClass); + assert valueType != null; // Else: Exception is thrown. + customDefaultValueTypes.register(fieldType, valueType); + } + + // WithValueTypeProvider annotations: + WithValueTypeProvider[] valueTypeProviderAnnotations = configClass.getAnnotationsByType(WithValueTypeProvider.class); + assert valueTypeProviderAnnotations != null; + for (WithValueTypeProvider valueTypeProviderAnnotation : valueTypeProviderAnnotations) { + Class valueTypeProviderClass = valueTypeProviderAnnotation.value(); + assert valueTypeProviderClass != null; + ValueTypeProvider valueTypeProvider = instantiateValueTypeProvider(valueTypeProviderClass); + assert valueTypeProvider != null; // Else: Exception is thrown. + customDefaultValueTypes.register(valueTypeProvider); + } + } + + private static ValueTypeProvider instantiateValueTypeProvider(Class valueTypeProviderClass) { + assert valueTypeProviderClass != null; + try { + return valueTypeProviderClass.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Could not instantiate ValueTypeProvider: " + valueTypeProviderClass.getName(), e); + } + } + + // SETTINGS + + /** + * Gets the setting fields. + * + * @return an unmodifiable view on the setting fields + */ + protected final Collection getSettings() { + this.setupSettings(); + return settings; + } + + protected String getConfigKey(Field field) { + return ConfigHelper.toConfigKey(field.getName()); + } + + protected final Field getSetting(String configKey) { + for (Field field : this.getSettings()) { + if (this.getConfigKey(field).equals(configKey)) { + return field; + } + } + return null; + } + + // Assert: Does not return null. Expects a valid setting field. + @SuppressWarnings("unchecked") + protected final ValueType getValueType(Field field) { + this.setupSettings(); + ValueType valueType = (ValueType) valueTypes.get(field); + Validate.notNull(valueType, "Field is not a setting: " + field.getName()); + return valueType; + } + + // LOADING + + public void load(ConfigurationSection config) throws ConfigLoadException { + Validate.notNull(config, "config is null"); + this.validateConfig(config); + for (Field field : this.getSettings()) { + this.loadSetting(field, config); + } + this.validateSettings(); + } + + protected void validateConfig(ConfigurationSection config) throws ConfigLoadException { + } + + protected void loadSetting(Field field, ConfigurationSection config) throws ConfigLoadException { + String configKey = this.getConfigKey(field); + ValueType valueType = this.getValueType(field); + try { + T value = null; + + // Handle missing value: + if (!config.isSet(configKey)) { + // We use the default value, if there is one: + value = this.getDefaultValue(field, config); // Can be null + this.onValueMissing(field, config, value); + } else { + // Load value: + value = valueType.load(config, configKey); + assert value != null; // We expect an exception if the value cannot be loaded + } + + // Apply value: + if (value != null) { + this.setSetting(field, value); + } // Else: Retain previous value. + } catch (SettingLoadException e) { + this.onSettingLoadException(field, config, e); + } + } + + protected void onValueMissing(Field field, ConfigurationSection config, T defaultValue) throws ConfigLoadException { + String configKey = this.getConfigKey(field); + if (defaultValue == null) { + Log.warning(this.msgMissingValue(configKey)); + } else { + Log.warning(this.msgUsingDefaultForMissingValue(configKey, defaultValue)); + } + } + + protected String msgMissingValue(String configKey) { + return this.getLogPrefix() + "Missing config entry: " + configKey; + } + + protected String msgUsingDefaultForMissingValue(String configKey, Object defaultValue) { + return this.getLogPrefix() + "Using default value for missing config entry: " + configKey; + } + + protected void onSettingLoadException(Field field, ConfigurationSection config, SettingLoadException e) throws ConfigLoadException { + String configKey = this.getConfigKey(field); + Log.warning(this.msgSettingLoadException(configKey, e)); + for (String extraMessage : e.getExtraMessages()) { + Log.warning(this.getLogPrefix() + extraMessage); + } + } + + protected String msgSettingLoadException(String configKey, SettingLoadException e) { + return this.getLogPrefix() + "Could not load setting '" + configKey + "': " + e.getMessage(); + } + + protected void setSetting(Field field, Object value) throws SettingLoadException { + if (value != null) { + Class fieldType = field.getType(); + if (!Utils.isAssignableFrom(fieldType, value.getClass())) { + throw new SettingLoadException("Value is of wrong type: Got " + value.getClass().getName() + ", expected " + fieldType.getName()); + } + } + + boolean accessible = field.isAccessible(); + try { + if (!accessible) { + // Temporarily set the field accessible, for example for private fields: + field.setAccessible(true); + } + // Note: The config instance is ignored if the field is static. + field.set(this, value); + } catch (Exception e) { + throw new SettingLoadException(e.getMessage(), e); + } finally { + // Restore previous accessible state: + try { + field.setAccessible(accessible); + } catch (SecurityException e) { + } + } + } + + /** + * Validation once all settings have been loaded. + */ + protected void validateSettings() { + } + + // DEFAULT VALUES + + // Checks whether there are default values available. + // This has to be consistent with #getDefaultValue. + protected boolean hasDefaultValues(ConfigurationSection config) { + return config instanceof Configuration && ((Configuration) config).getDefaults() != null; + } + + // Returns null if there is no default value. + @SuppressWarnings("unchecked") + protected T getDefaultValue(Field field, ConfigurationSection config) { + Configuration defaults = null; + if (config instanceof Configuration) { + defaults = ((Configuration) config).getDefaults(); + } + if (defaults == null) { + // No default config values available. + return null; + } + + String configKey = this.getConfigKey(field); + ValueType valueType = this.getValueType(field); + + // Load default value: + try { + // Note: This can return null if the default config does not contain a default value for this setting. + return (T) valueType.load(defaults, configKey); + } catch (SettingLoadException e) { + Log.warning(this.msgDefaultSettingLoadException(configKey, e)); + for (String extraMessage : e.getExtraMessages()) { + Log.warning(this.getLogPrefix() + extraMessage); + } + return null; + } + } + + protected String msgDefaultSettingLoadException(String configKey, SettingLoadException e) { + return this.getLogPrefix() + "Could not load default value for setting '" + configKey + "': " + e.getMessage(); + } + + /** + * Inserts the default values for missing settings into the given config. + * + * @param config + * the config + * @return true if any default values have been inserted + */ + protected boolean insertMissingDefaultValues(ConfigurationSection config) { + Validate.notNull(config, "config is null"); + if (!this.hasDefaultValues(config)) { + // No default config values available. + return false; + } + + // Initialize missing settings with their default value: + boolean configChanged = false; + for (Field field : this.getSettings()) { + if (this.insertMissingDefaultValue(config, field)) { + configChanged = true; + } + } + return configChanged; + } + + // Returns true if the default value got inserted. + protected boolean insertMissingDefaultValue(ConfigurationSection config, Field field) { + assert this.hasDefaultValues(config); + String configKey = this.getConfigKey(field); + if (config.isSet(configKey)) return false; // Not missing. + + Log.warning(this.msgInsertingDefault(configKey)); + + // Get default value: + T defaultValue = this.getDefaultValue(field, config); + if (defaultValue == null) { + Log.warning(this.msgMissingDefault(configKey)); + return false; + } + + // Save default value to config: + ValueType valueType = this.getValueType(field); + valueType.save(config, configKey, defaultValue); + return true; + } + + protected String msgInsertingDefault(String configKey) { + return this.getLogPrefix() + "Inserting default value for missing config entry: " + configKey; + } + + protected String msgMissingDefault(String configKey) { + return this.getLogPrefix() + "Missing default value for setting: " + configKey; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/ConfigHelper.java b/src/main/java/com/nisovin/shopkeepers/config/ConfigHelper.java index d9d74fb1..5322df20 100644 --- a/src/main/java/com/nisovin/shopkeepers/config/ConfigHelper.java +++ b/src/main/java/com/nisovin/shopkeepers/config/ConfigHelper.java @@ -1,21 +1,7 @@ package com.nisovin.shopkeepers.config; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Locale; -import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.bukkit.Material; -import org.bukkit.configuration.Configuration; - -import com.nisovin.shopkeepers.text.Text; -import com.nisovin.shopkeepers.util.ConfigUtils; -import com.nisovin.shopkeepers.util.ItemData; -import com.nisovin.shopkeepers.util.Log; -import com.nisovin.shopkeepers.util.TextUtils; public class ConfigHelper { @@ -54,119 +40,4 @@ public class ConfigHelper { public static String toConfigKey(String fieldName) { return CONFIG_KEY_PATTERN.matcher(fieldName).replaceAll("-$1").toLowerCase(Locale.ROOT); } - - public static Object loadConfigValue(Configuration config, String configKey, Set noColorConversionKeys, Class typeClass, Class genericType) { - if (typeClass == String.class || typeClass == Text.class) { - String string = config.getString(configKey); - // Colorize, if not exempted: - if (!noColorConversionKeys.contains(configKey)) { - string = TextUtils.colorize(string); - } - if (typeClass == Text.class) { - return Text.parse(string); - } else { - return string; - } - } else if (typeClass == int.class) { - return config.getInt(configKey); - } else if (typeClass == short.class) { - return (short) config.getInt(configKey); - } else if (typeClass == boolean.class) { - return config.getBoolean(configKey); - } else if (typeClass == Material.class) { - // This assumes that legacy item conversion has already been performed - Material material = ConfigUtils.loadMaterial(config, configKey); - if (material == null) { - Log.warning("Config: Unknown material for config entry '" + configKey + "': " + config.get(configKey)); - Log.warning("Config: All valid material names can be found here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html"); - } - return material; - } else if (typeClass == ItemData.class) { - ItemData itemData = loadItemData(config.get(configKey), configKey); - // Normalize to not null: - if (itemData == null) { - itemData = new ItemData(Material.AIR); - } - return itemData; - } else if (typeClass == List.class) { - if (genericType == String.class || genericType == Text.class) { - List stringList = config.getStringList(configKey); - // Colorize, if not exempted: - if (!noColorConversionKeys.contains(configKey)) { - stringList = TextUtils.colorize(stringList); - } - if (genericType == Text.class) { - return Text.parse(stringList); - } else { - return stringList; - } - } else if (genericType == ItemData.class) { - List list = config.getList(configKey, Collections.emptyList()); - List itemDataList = new ArrayList<>(list.size()); - int index = 0; - for (Object entry : list) { - index += 1; - ItemData itemData = loadItemData(entry, configKey + "[" + index + "]"); - if (itemData != null) { - itemDataList.add(itemData); - } - } - return itemDataList; - } else { - throw new IllegalStateException("Unsupported config setting list type: " + genericType.getName()); - } - } - throw new IllegalStateException("Unsupported config setting type: " + typeClass.getName()); - } - - public static ItemData loadItemData(Object dataObject, String configEntryIdentifier) { - ItemData itemData = ItemData.deserialize(dataObject, (warning) -> { - Log.warning("Config: Couldn't load item data for config entry '" + configEntryIdentifier + "': " + warning); - if (warning.contains("Unknown item type")) { // TODO this is ugly - Log.warning("Config: All valid material names can be found here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html"); - } - }); - return itemData; - } - - public static void setConfigValue(Configuration config, String configKey, Set noColorConversionKeys, Class typeClass, Class genericType, Object value) { - if (value == null) { - // Remove value: - config.set(configKey, null); - return; - } - - if (typeClass == Material.class) { - config.set(configKey, ((Material) value).name()); - } else if (typeClass == String.class || typeClass == Text.class) { - String stringValue; - if (typeClass == Text.class) { - stringValue = ((Text) value).toPlainFormatText(); - } else { - stringValue = (String) value; - } - // Decolorize, if not exempted: - if (!noColorConversionKeys.contains(configKey)) { - value = TextUtils.decolorize(stringValue); - } - config.set(configKey, value); - } else if (typeClass == List.class && (genericType == String.class || genericType == Text.class)) { - List stringList; - if (genericType == Text.class) { - stringList = ((List) value).stream().map(Text::toPlainFormatText).collect(Collectors.toList()); - } else { - stringList = (List) value; - } - - // Decolorize, if not exempted: - if (!noColorConversionKeys.contains(configKey)) { - value = TextUtils.decolorize(stringList); - } - config.set(configKey, value); - } else if (typeClass == ItemData.class) { - config.set(configKey, ((ItemData) value).serialize()); - } else { - config.set(configKey, value); - } - } } diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/Colored.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/Colored.java new file mode 100644 index 00000000..ba8b5bb7 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/Colored.java @@ -0,0 +1,17 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.nisovin.shopkeepers.config.value.types.ColoredStringListValue; +import com.nisovin.shopkeepers.config.value.types.ColoredStringValue; + +/** + * Shortcut for using {@link ColoredStringValue} or {@link ColoredStringListValue}. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Colored { +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/Uncolored.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/Uncolored.java new file mode 100644 index 00000000..6acd17e5 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/Uncolored.java @@ -0,0 +1,17 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.nisovin.shopkeepers.config.value.types.StringListValue; +import com.nisovin.shopkeepers.config.value.types.StringValue; + +/** + * Shortcut for using {@link StringValue} or {@link StringListValue}. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Uncolored { +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/WithDefaultValueType.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithDefaultValueType.java new file mode 100644 index 00000000..eda3b96d --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithDefaultValueType.java @@ -0,0 +1,31 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.nisovin.shopkeepers.config.value.DefaultValueTypes; +import com.nisovin.shopkeepers.config.value.ValueType; + +/** + * Specifies the default {@link ValueType} to use for config fields of a specific type. + *

+ * This overrides the defaults specified by {@link DefaultValueTypes}. + *

+ * The specified value type has to provide a no-args constructor. + *

+ * Default value types specified by this annotation are inherited to subclasses and can be overridden there by other + * annotations for the same field type. If multiple default value types are specified for the same field type, the first + * one is used. + */ +@Target(ElementType.TYPE) +@Repeatable(WithDefaultValueTypes.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithDefaultValueType { + + Class fieldType(); + + Class> valueType(); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/WithDefaultValueTypes.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithDefaultValueTypes.java new file mode 100644 index 00000000..4f0fb2db --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithDefaultValueTypes.java @@ -0,0 +1,16 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation for multiple instances of {@link WithDefaultValueType}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@interface WithDefaultValueTypes { + + WithDefaultValueType[] value(); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueType.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueType.java new file mode 100644 index 00000000..bd038be4 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueType.java @@ -0,0 +1,20 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.nisovin.shopkeepers.config.value.ValueType; + +/** + * Specifies the {@link ValueType} to use for a specific config field. + *

+ * The specified value type has to provide a no-args constructor. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithValueType { + + Class> value(); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueTypeProvider.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueTypeProvider.java new file mode 100644 index 00000000..169331e6 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueTypeProvider.java @@ -0,0 +1,29 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.nisovin.shopkeepers.config.value.DefaultValueTypes; +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.ValueTypeProvider; + +/** + * Specifies a {@link ValueTypeProvider} to use for config fields. + *

+ * This overrides the defaults specified by {@link DefaultValueTypes}. + *

+ * The specified value type provider has to provide a no-args constructor. + *

+ * Default value type providers specified by this annotation are inherited to subclasses. The first provider (starting + * with those specified at the most specific class) that can provide a {@link ValueType} is used. + */ +@Target(ElementType.TYPE) +@Repeatable(WithValueTypeProviders.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithValueTypeProvider { + + Class value(); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueTypeProviders.java b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueTypeProviders.java new file mode 100644 index 00000000..8142e834 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/annotation/WithValueTypeProviders.java @@ -0,0 +1,16 @@ +package com.nisovin.shopkeepers.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation for multiple instances of {@link WithValueTypeProvider}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@interface WithValueTypeProviders { + + WithValueTypeProvider[] value(); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/DefaultValueTypes.java b/src/main/java/com/nisovin/shopkeepers/config/value/DefaultValueTypes.java new file mode 100644 index 00000000..cd4748ad --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/DefaultValueTypes.java @@ -0,0 +1,60 @@ +package com.nisovin.shopkeepers.config.value; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +import org.bukkit.Material; + +import com.nisovin.shopkeepers.config.value.types.BooleanValue; +import com.nisovin.shopkeepers.config.value.types.DoubleValue; +import com.nisovin.shopkeepers.config.value.types.IntegerValue; +import com.nisovin.shopkeepers.config.value.types.ItemDataValue; +import com.nisovin.shopkeepers.config.value.types.ListValue; +import com.nisovin.shopkeepers.config.value.types.LongValue; +import com.nisovin.shopkeepers.config.value.types.MaterialValue; +import com.nisovin.shopkeepers.config.value.types.StringValue; +import com.nisovin.shopkeepers.config.value.types.TextValue; +import com.nisovin.shopkeepers.text.Text; +import com.nisovin.shopkeepers.util.ItemData; + +/** + * Registry of default value types of settings. + */ +public class DefaultValueTypes { + + private static final ValueTypeRegistry registry = new ValueTypeRegistry(); + + static { + registry.register(String.class, StringValue.INSTANCE); + registry.register(Boolean.class, BooleanValue.INSTANCE); + registry.register(boolean.class, BooleanValue.INSTANCE); + registry.register(Integer.class, IntegerValue.INSTANCE); + registry.register(int.class, IntegerValue.INSTANCE); + registry.register(Double.class, DoubleValue.INSTANCE); + registry.register(double.class, DoubleValue.INSTANCE); + registry.register(Long.class, LongValue.INSTANCE); + registry.register(long.class, LongValue.INSTANCE); + + registry.register(Text.class, TextValue.INSTANCE); + registry.register(Material.class, MaterialValue.INSTANCE); + registry.register(ItemData.class, ItemDataValue.INSTANCE); + + registry.register(ValueTypeProviders.forTypePattern(TypePatterns.forClass(List.class), (type) -> { + assert type instanceof ParameterizedType; + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + ValueType elementValueType = DefaultValueTypes.get(elementType); + if (elementValueType == null) { + throw new IllegalArgumentException("Unsupported element type: " + elementType.getTypeName()); + } + return new ListValue<>(elementValueType); + })); + } + + public static ValueType get(Type type) { + return registry.getValueType(type); + } + + private DefaultValueTypes() { + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/SettingLoadException.java b/src/main/java/com/nisovin/shopkeepers/config/value/SettingLoadException.java new file mode 100644 index 00000000..dc2d864d --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/SettingLoadException.java @@ -0,0 +1,33 @@ +package com.nisovin.shopkeepers.config.value; + +import java.util.Collections; +import java.util.List; + +public class SettingLoadException extends Exception { + + private static final long serialVersionUID = -3068903999888105245L; + + private final List extraMessages; + + public SettingLoadException(String message) { + this(message, Collections.emptyList()); + } + + public SettingLoadException(String message, List extraMessages) { + super(message); + this.extraMessages = extraMessages; + } + + public SettingLoadException(String message, Throwable cause) { + this(message, Collections.emptyList(), cause); + } + + public SettingLoadException(String message, List extraMessages, Throwable cause) { + super(message, cause); + this.extraMessages = extraMessages; + } + + public List getExtraMessages() { + return extraMessages; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/TypePattern.java b/src/main/java/com/nisovin/shopkeepers/config/value/TypePattern.java new file mode 100644 index 00000000..b841e846 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/TypePattern.java @@ -0,0 +1,9 @@ +package com.nisovin.shopkeepers.config.value; + +import java.lang.reflect.Type; + +public interface TypePattern { + + public boolean matches(Type type); + +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/TypePatterns.java b/src/main/java/com/nisovin/shopkeepers/config/value/TypePatterns.java new file mode 100644 index 00000000..4c9e68e8 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/TypePatterns.java @@ -0,0 +1,104 @@ +package com.nisovin.shopkeepers.config.value; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import com.nisovin.shopkeepers.util.Validate; + +public class TypePatterns { + + public static TypePattern forClass(Class clazz) { + return new ClassTypePattern(clazz); + } + + private static class ClassTypePattern implements TypePattern { + + private final Class clazz; + + public ClassTypePattern(Class clazz) { + Validate.notNull(clazz, "clazz is null"); + this.clazz = clazz; + } + + @Override + public boolean matches(Type type) { + if (this.clazz == type) return true; + if (type instanceof ParameterizedType) { + if (this.clazz == ((ParameterizedType) type).getRawType()) { + return true; + } + } + return false; + } + } + + public static TypePattern parameterized(Class clazz, TypePattern... typeParameters) { + return new ParameterizedTypePattern(clazz, typeParameters); + } + + public static TypePattern parameterized(Class clazz, Class... typeParameters) { + TypePattern[] typePatterns = null; + if (typeParameters != null) { + typePatterns = new TypePattern[typeParameters.length]; + for (int i = 0; i < typeParameters.length; ++i) { + Class typeParameter = typeParameters[i]; + Validate.notNull(typeParameter, "One of the typeParameters is null!"); + typePatterns[i] = TypePatterns.forClass(typeParameter); + } + } + return parameterized(clazz, typePatterns); + } + + private static class ParameterizedTypePattern extends ClassTypePattern { + + private final TypePattern[] typeParameters; + + public ParameterizedTypePattern(Class clazz, TypePattern... typeParameters) { + super(clazz); + Validate.notNull(typeParameters, "typeParameters is null"); + Validate.notNull(typeParameters.length == 0, "typeParameters is empty"); + this.typeParameters = (typeParameters == null) ? null : typeParameters.clone(); + } + + @Override + public boolean matches(Type type) { + if (!super.matches(type)) return false; + if (!(type instanceof ParameterizedType)) { + return false; + } + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + if (typeArguments == null || typeArguments.length != this.typeParameters.length) { + return false; + } + for (int i = 0; i < typeParameters.length; ++i) { + if (!this.typeParameters[i].matches(typeArguments[i])) { + return false; + } + } + return true; + } + } + + public static TypePattern any() { + return AnyTypePattern.INSTANCE; + } + + /** + * Matches any type. + */ + private static class AnyTypePattern implements TypePattern { + + public static final AnyTypePattern INSTANCE = new AnyTypePattern(); + + public AnyTypePattern() { + } + + @Override + public boolean matches(Type type) { + return true; + } + } + + private TypePatterns() { + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/ValueType.java b/src/main/java/com/nisovin/shopkeepers/config/value/ValueType.java new file mode 100644 index 00000000..4586e0ec --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/ValueType.java @@ -0,0 +1,50 @@ +package com.nisovin.shopkeepers.config.value; + +import org.bukkit.configuration.ConfigurationSection; + +import com.nisovin.shopkeepers.config.annotation.WithDefaultValueType; +import com.nisovin.shopkeepers.config.annotation.WithValueType; + +/** + * Defines how values of this type are loaded from and saved to configs. + *

+ * Subtypes should ideally provide a public no-args constructor so that they can be used together with the + * {@link WithValueType} and {@link WithDefaultValueType} annotations. + * + * @param + * the type of value + */ +public abstract class ValueType { + + public ValueType() { + } + + public T load(ConfigurationSection config, String key) throws SettingLoadException { + Object configValue = config.get(key); + return this.load(configValue); + } + + public T load(ConfigurationSection config, String key, T defaultValue) { + T value = null; + try { + value = this.load(config, key); + } catch (SettingLoadException e) { + } + if (value == null) { + return defaultValue; + } else { + return value; + } + } + + // Null indicates the absence of a value and should only be used if the input has been null. + public abstract T load(Object configValue) throws SettingLoadException; + + // A value of null will clear the config entry. + public void save(ConfigurationSection config, String key, T value) { + Object configValue = this.save(value); // Can be null + config.set(key, configValue); + } + + public abstract Object save(T value); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeProvider.java b/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeProvider.java new file mode 100644 index 00000000..cc0d14d6 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeProvider.java @@ -0,0 +1,18 @@ +package com.nisovin.shopkeepers.config.value; + +import java.lang.reflect.Type; + +public interface ValueTypeProvider { + + /** + * Gets the {@link ValueType} for the given setting type, if it can provide one. + *

+ * The returned {@link ValueType} may get cached and used for future requests for the same setting type. The + * provider is therefore only allowed to take the type itself into account, and not any other contextual state. + * + * @param type + * the type of the setting's value + * @return the value type, or null if none can be provided + */ + public ValueType get(Type type); +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeProviders.java b/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeProviders.java new file mode 100644 index 00000000..0209cf67 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeProviders.java @@ -0,0 +1,27 @@ +package com.nisovin.shopkeepers.config.value; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import com.nisovin.shopkeepers.util.Validate; + +public class ValueTypeProviders { + + public static ValueTypeProvider forTypePattern(TypePattern typePattern, Function> valueTypeProvider) { + Validate.notNull(typePattern, "typePattern is null"); + Validate.notNull(valueTypeProvider, "valueTypeProvider is null"); + return new ValueTypeProvider() { + @Override + public ValueType get(Type type) { + if (typePattern.matches(type)) { + return valueTypeProvider.apply(type); + } else { + return null; + } + } + }; + } + + private ValueTypeProviders() { + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeRegistry.java b/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeRegistry.java new file mode 100644 index 00000000..1f7a82fb --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/ValueTypeRegistry.java @@ -0,0 +1,55 @@ +package com.nisovin.shopkeepers.config.value; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.nisovin.shopkeepers.util.Validate; + +/** + * Registry of value types for setting types. + */ +public class ValueTypeRegistry { + + private final Map> byType = new HashMap<>(); + // Ordered: The first successful provider is used. + private final List providers = new ArrayList<>(); + + public ValueTypeRegistry() { + } + + // Replaces any previously registered ValueType. + public void register(Type type, ValueType valueType) { + Validate.notNull(type, "type is null"); + Validate.notNull(valueType, "valueType is null"); + byType.put(type, valueType); + } + + public boolean hasCachedValueType(Type type) { + return byType.containsKey(type); + } + + public void register(ValueTypeProvider valueTypeProvider) { + Validate.notNull(valueTypeProvider, "valueTypeProvider is null"); + providers.add(valueTypeProvider); + } + + @SuppressWarnings("unchecked") + public ValueType getValueType(Type type) { + ValueType valueType = (ValueType) byType.get(type); + if (valueType == null) { + // Check providers: + for (ValueTypeProvider provider : providers) { + valueType = (ValueType) provider.get(type); + if (valueType != null) { + // Cache result: + this.register(type, valueType); + break; + } // Else: Continue searching. + } + } + return valueType; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/BooleanValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/BooleanValue.java new file mode 100644 index 00000000..7c246a64 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/BooleanValue.java @@ -0,0 +1,28 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.util.ConversionUtils; + +public class BooleanValue extends ValueType { + + public static final BooleanValue INSTANCE = new BooleanValue(); + + public BooleanValue() { + } + + @Override + public Boolean load(Object configValue) throws SettingLoadException { + if (configValue == null) return null; + Boolean value = ConversionUtils.toBoolean(configValue); + if (value == null) { + throw new SettingLoadException("Invalid boolean value: " + configValue); + } + return value; + } + + @Override + public Object save(Boolean value) { + return value; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/ColoredStringListValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/ColoredStringListValue.java new file mode 100644 index 00000000..5244e0d2 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/ColoredStringListValue.java @@ -0,0 +1,28 @@ +package com.nisovin.shopkeepers.config.value.types; + +import java.lang.reflect.Type; +import java.util.List; + +import com.nisovin.shopkeepers.config.value.TypePattern; +import com.nisovin.shopkeepers.config.value.TypePatterns; +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.ValueTypeProvider; +import com.nisovin.shopkeepers.config.value.ValueTypeProviders; + +public class ColoredStringListValue extends ListValue { + + public static final ColoredStringListValue INSTANCE = new ColoredStringListValue(); + public static final TypePattern TYPE_PATTERN = TypePatterns.parameterized(List.class, String.class); + public static final ValueTypeProvider PROVIDER = ValueTypeProviders.forTypePattern(TYPE_PATTERN, type -> INSTANCE); + + public static final class Provider implements ValueTypeProvider { + @Override + public ValueType get(Type type) { + return PROVIDER.get(type); + } + } + + public ColoredStringListValue() { + super(ColoredStringValue.INSTANCE); + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/ColoredStringValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/ColoredStringValue.java new file mode 100644 index 00000000..59c36f6f --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/ColoredStringValue.java @@ -0,0 +1,22 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.util.TextUtils; + +public class ColoredStringValue extends StringValue { + + public static final ColoredStringValue INSTANCE = new ColoredStringValue(); + + public ColoredStringValue() { + } + + @Override + public String load(Object configValue) throws SettingLoadException { + return TextUtils.colorize(super.load(configValue)); + } + + @Override + public Object save(String value) { + return TextUtils.decolorize(value); + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/DoubleValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/DoubleValue.java new file mode 100644 index 00000000..acb50869 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/DoubleValue.java @@ -0,0 +1,28 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.util.ConversionUtils; + +public class DoubleValue extends ValueType { + + public static final DoubleValue INSTANCE = new DoubleValue(); + + public DoubleValue() { + } + + @Override + public Double load(Object configValue) throws SettingLoadException { + if (configValue == null) return null; + Double value = ConversionUtils.toDouble(configValue); + if (value == null) { + throw new SettingLoadException("Invalid double value: " + configValue); + } + return value; + } + + @Override + public Object save(Double value) { + return value; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/IntegerValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/IntegerValue.java new file mode 100644 index 00000000..0b2b9bb9 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/IntegerValue.java @@ -0,0 +1,28 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.util.ConversionUtils; + +public class IntegerValue extends ValueType { + + public static final IntegerValue INSTANCE = new IntegerValue(); + + public IntegerValue() { + } + + @Override + public Integer load(Object configValue) throws SettingLoadException { + if (configValue == null) return null; + Integer value = ConversionUtils.toInteger(configValue); + if (value == null) { + throw new SettingLoadException("Invalid integer value: " + configValue); + } + return value; + } + + @Override + public Object save(Integer value) { + return value; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/ItemDataValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/ItemDataValue.java new file mode 100644 index 00000000..a7db4c52 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/ItemDataValue.java @@ -0,0 +1,50 @@ +package com.nisovin.shopkeepers.config.value.types; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.util.ItemData; + +public class ItemDataValue extends ValueType { + + public static final ItemDataValue INSTANCE = new ItemDataValue(); + + public ItemDataValue() { + } + + @Override + public ItemData load(Object configValue) throws SettingLoadException { + ItemData itemData = null; + try { + // Returns null if the config value is null. Otherwise triggers a warning, which we translate into an + // exception. + itemData = ItemData.deserialize(configValue, (warning) -> { + List extraMessages = Collections.emptyList(); + if (warning.contains("Unknown item type")) { // TODO this is ugly + extraMessages = Arrays.asList( + "All valid material names can be found here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html" + ); + } + // We can only throw unchecked exceptions here, so we wrap the exception here and unwrap it again + // outside: + throw new RuntimeException(new SettingLoadException(warning, extraMessages)); + }); + } catch (RuntimeException e) { + if (e.getCause() instanceof SettingLoadException) { + throw (SettingLoadException) e.getCause(); + } else { + throw e; + } + } + return itemData; + } + + @Override + public Object save(ItemData value) { + if (value == null) return null; + return value.serialize(); + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/ListValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/ListValue.java new file mode 100644 index 00000000..9c0eea9a --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/ListValue.java @@ -0,0 +1,42 @@ +package com.nisovin.shopkeepers.config.value.types; + +import java.util.ArrayList; +import java.util.List; + +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.util.Validate; + +public class ListValue extends ValueType> { + + private final ValueType elementValueType; + + public ListValue(ValueType elementValueType) { + Validate.notNull(elementValueType, "elementValueType is null!"); + this.elementValueType = elementValueType; + } + + @Override + public List load(Object configValue) throws SettingLoadException { + if (configValue == null) return null; + if (!(configValue instanceof List)) { + throw new SettingLoadException("Expecting a list of values, but got " + configValue.getClass().getName()); + } + List configValues = (List) configValue; + List values = new ArrayList<>(configValues.size()); + for (Object configElement : configValues) { + values.add(elementValueType.load(configElement)); + } + return values; + } + + @Override + public Object save(List value) { + if (value == null) return null; + List configValues = new ArrayList<>(value.size()); + for (E element : value) { + configValues.add(elementValueType.save(element)); + } + return configValues; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/LongValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/LongValue.java new file mode 100644 index 00000000..9672170d --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/LongValue.java @@ -0,0 +1,28 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.util.ConversionUtils; + +public class LongValue extends ValueType { + + public static final LongValue INSTANCE = new LongValue(); + + public LongValue() { + } + + @Override + public Long load(Object configValue) throws SettingLoadException { + if (configValue == null) return null; + Long value = ConversionUtils.toLong(configValue); + if (value == null) { + throw new SettingLoadException("Invalid long value: " + configValue); + } + return value; + } + + @Override + public Object save(Long value) { + return value; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/MaterialValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/MaterialValue.java new file mode 100644 index 00000000..8abca19c --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/MaterialValue.java @@ -0,0 +1,38 @@ +package com.nisovin.shopkeepers.config.value.types; + +import java.util.Arrays; + +import org.bukkit.Material; + +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.ValueType; + +public class MaterialValue extends ValueType { + + public static final MaterialValue INSTANCE = new MaterialValue(); + + private static final StringValue stringValue = new StringValue(); + + public MaterialValue() { + } + + @Override + public Material load(Object configValue) throws SettingLoadException { + String materialName = stringValue.load(configValue); + if (materialName == null) return null; + // This assumes that legacy item conversion has already been performed: + Material material = Material.matchMaterial(materialName); // Can be null + if (material == null) { + throw new SettingLoadException("Unknown material: " + materialName, Arrays.asList( + "All valid material names can be found here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html" + )); + } + return material; + } + + @Override + public Object save(Material value) { + if (value == null) return null; + return value.name(); + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/StringListValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/StringListValue.java new file mode 100644 index 00000000..40e148a4 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/StringListValue.java @@ -0,0 +1,10 @@ +package com.nisovin.shopkeepers.config.value.types; + +public class StringListValue extends ListValue { + + public static final StringListValue INSTANCE = new StringListValue(); + + public StringListValue() { + super(StringValue.INSTANCE); + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/StringValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/StringValue.java new file mode 100644 index 00000000..8a0bccc0 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/StringValue.java @@ -0,0 +1,23 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.ValueType; + +public class StringValue extends ValueType { + + public static final StringValue INSTANCE = new StringValue(); + + public StringValue() { + } + + @Override + public String load(Object configValue) throws SettingLoadException { + if (configValue == null) return null; + return configValue.toString(); + } + + @Override + public Object save(String value) { + return value; + } +} diff --git a/src/main/java/com/nisovin/shopkeepers/config/value/types/TextValue.java b/src/main/java/com/nisovin/shopkeepers/config/value/types/TextValue.java new file mode 100644 index 00000000..55d88856 --- /dev/null +++ b/src/main/java/com/nisovin/shopkeepers/config/value/types/TextValue.java @@ -0,0 +1,26 @@ +package com.nisovin.shopkeepers.config.value.types; + +import com.nisovin.shopkeepers.config.value.SettingLoadException; +import com.nisovin.shopkeepers.config.value.ValueType; +import com.nisovin.shopkeepers.text.Text; + +public class TextValue extends ValueType { + + public static final TextValue INSTANCE = new TextValue(); + + private static final ColoredStringValue coloredStringValue = new ColoredStringValue(); + + public TextValue() { + } + + @Override + public Text load(Object configValue) throws SettingLoadException { + return Text.parse(coloredStringValue.load(configValue)); + } + + @Override + public Object save(Text value) { + if (value == null) return null; + return coloredStringValue.save(value.toPlainFormatText()); + } +}