Major refactoring related to how the config and language files are loaded.

master
blablubbabc 2020-11-28 01:27:11 +01:00
parent 68dbd0c521
commit f9c565b110
32 changed files with 1448 additions and 272 deletions

View File

@ -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:

View File

@ -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<String> 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<String> 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<String> messageKeys = this.getSettings().stream().map(this::getConfigKey).collect(Collectors.toSet());
Set<String> configKeys = config.getKeys(false);
for (String configKey : configKeys) {
if (!messageKeys.contains(configKey)) {
Log.warning(this.getLogPrefix() + "Unknown message: " + configKey);
}
}
}
}

View File

@ -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<String> 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() {
}
}

View File

@ -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<Field, ValueType<?>> valueTypes = null;
private Collection<Field> 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<Field> streamSettings() {
Class<?> configClass = this.getClass();
Stream<Field> 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<Field> streamSettings(Class<?> configClass) {
List<Field> 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.
* <p>
* By default, all public fields are included. Fields can also be declared in parent config classes.
* <p>
* Synthetic and final fields are always excluded.
*
* @param field
* the field
* @return <code>true</code> if the field is a setting, <code>false</code> otherwise
*/
protected boolean isSetting(Field field) {
if (!Modifier.isPublic(field.getModifiers())) {
return false;
}
return true;
}
// Assert: Does not return null.
protected <T> ValueType<T> setupValueType(Field field) {
ValueType<T> 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 <T> ValueType<T> getValueTypeByAnnotation(Field field) {
WithValueType valueTypeAnnotation = field.getAnnotation(WithValueType.class);
if (valueTypeAnnotation != null) {
Class<? extends ValueType<?>> valueTypeClass = valueTypeAnnotation.value();
assert valueTypeClass != null;
return (ValueType<T>) instantiateValueType(valueTypeClass);
}
return null;
}
private static ValueType<?> instantiateValueType(Class<? extends ValueType<?>> 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 <T> ValueType<T> getValueTypeByColoredAnnotation(Field field) {
Colored coloredAnnotation = field.getAnnotation(Colored.class);
if (coloredAnnotation != null) {
Type fieldType = field.getGenericType();
if (fieldType == String.class) {
return (ValueType<T>) ColoredStringValue.INSTANCE;
} else if (ColoredStringListValue.TYPE_PATTERN.matches(fieldType)) {
return (ValueType<T>) ColoredStringListValue.INSTANCE;
} else {
throw new IllegalArgumentException("The Colored annotation is not supported for settings of type " + fieldType.getTypeName());
}
}
return null;
}
@SuppressWarnings("unchecked")
protected final <T> ValueType<T> getValueTypeByUncoloredAnnotation(Field field) {
Uncolored uncoloredAnnotation = field.getAnnotation(Uncolored.class);
if (uncoloredAnnotation != null) {
Type fieldType = field.getGenericType();
if (fieldType == String.class) {
return (ValueType<T>) StringValue.INSTANCE;
} else if (ColoredStringListValue.TYPE_PATTERN.matches(fieldType)) {
return (ValueType<T>) StringListValue.INSTANCE;
} else {
throw new IllegalArgumentException("The Uncolored annotation is not supported for settings of type " + fieldType.getTypeName());
}
}
return null;
}
protected final <T> ValueType<T> 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<? extends ValueType<?>> 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<? extends ValueTypeProvider> 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<? extends ValueTypeProvider> 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<Field> 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 <T> ValueType<T> getValueType(Field field) {
this.setupSettings();
ValueType<T> valueType = (ValueType<T>) 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 <T> void loadSetting(Field field, ConfigurationSection config) throws ConfigLoadException {
String configKey = this.getConfigKey(field);
ValueType<T> 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 <T> 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 <T> 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> 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 <code>true</code> 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 <T> 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<T> 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;
}
}

View File

@ -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<String> 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<String> 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<ItemData> 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<String> 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<String> stringList;
if (genericType == Text.class) {
stringList = ((List<Text>) value).stream().map(Text::toPlainFormatText).collect(Collectors.toList());
} else {
stringList = (List<String>) 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);
}
}
}

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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.
* <p>
* This overrides the defaults specified by {@link DefaultValueTypes}.
* <p>
* The specified value type has to provide a no-args constructor.
* <p>
* 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<? extends ValueType<?>> valueType();
}

View File

@ -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();
}

View File

@ -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.
* <p>
* The specified value type has to provide a no-args constructor.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WithValueType {
Class<? extends ValueType<?>> value();
}

View File

@ -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.
* <p>
* This overrides the defaults specified by {@link DefaultValueTypes}.
* <p>
* The specified value type provider has to provide a no-args constructor.
* <p>
* 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<? extends ValueTypeProvider> value();
}

View File

@ -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();
}

View File

@ -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 <T> ValueType<T> get(Type type) {
return registry.getValueType(type);
}
private DefaultValueTypes() {
}
}

View File

@ -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<String> extraMessages;
public SettingLoadException(String message) {
this(message, Collections.emptyList());
}
public SettingLoadException(String message, List<String> extraMessages) {
super(message);
this.extraMessages = extraMessages;
}
public SettingLoadException(String message, Throwable cause) {
this(message, Collections.emptyList(), cause);
}
public SettingLoadException(String message, List<String> extraMessages, Throwable cause) {
super(message, cause);
this.extraMessages = extraMessages;
}
public List<String> getExtraMessages() {
return extraMessages;
}
}

View File

@ -0,0 +1,9 @@
package com.nisovin.shopkeepers.config.value;
import java.lang.reflect.Type;
public interface TypePattern {
public boolean matches(Type type);
}

View File

@ -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() {
}
}

View File

@ -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.
* <p>
* 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 <T>
* the type of value
*/
public abstract class ValueType<T> {
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);
}

View File

@ -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.
* <p>
* 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 <code>null</code> if none can be provided
*/
public ValueType<?> get(Type type);
}

View File

@ -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<Type, ValueType<?>> 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() {
}
}

View File

@ -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<Type, ValueType<?>> byType = new HashMap<>();
// Ordered: The first successful provider is used.
private final List<ValueTypeProvider> providers = new ArrayList<>();
public ValueTypeRegistry() {
}
// Replaces any previously registered ValueType.
public <T> void register(Type type, ValueType<? extends T> 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 <T> ValueType<T> getValueType(Type type) {
ValueType<T> valueType = (ValueType<T>) byType.get(type);
if (valueType == null) {
// Check providers:
for (ValueTypeProvider provider : providers) {
valueType = (ValueType<T>) provider.get(type);
if (valueType != null) {
// Cache result:
this.register(type, valueType);
break;
} // Else: Continue searching.
}
}
return valueType;
}
}

View File

@ -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<Boolean> {
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;
}
}

View File

@ -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<String> {
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);
}
}

View File

@ -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);
}
}

View File

@ -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<Double> {
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;
}
}

View File

@ -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<Integer> {
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;
}
}

View File

@ -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<ItemData> {
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<String> 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();
}
}

View File

@ -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<E> extends ValueType<List<E>> {
private final ValueType<E> elementValueType;
public ListValue(ValueType<E> elementValueType) {
Validate.notNull(elementValueType, "elementValueType is null!");
this.elementValueType = elementValueType;
}
@Override
public List<E> 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<E> values = new ArrayList<>(configValues.size());
for (Object configElement : configValues) {
values.add(elementValueType.load(configElement));
}
return values;
}
@Override
public Object save(List<E> value) {
if (value == null) return null;
List<Object> configValues = new ArrayList<>(value.size());
for (E element : value) {
configValues.add(elementValueType.save(element));
}
return configValues;
}
}

View File

@ -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<Long> {
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;
}
}

View File

@ -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<Material> {
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();
}
}

View File

@ -0,0 +1,10 @@
package com.nisovin.shopkeepers.config.value.types;
public class StringListValue extends ListValue<String> {
public static final StringListValue INSTANCE = new StringListValue();
public StringListValue() {
super(StringValue.INSTANCE);
}
}

View File

@ -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<String> {
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;
}
}

View File

@ -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<Text> {
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());
}
}