716 lines
33 KiB
Java
716 lines
33 KiB
Java
package world.bentobox.bentobox.database.yaml;
|
|
|
|
import java.beans.IntrospectionException;
|
|
import java.beans.PropertyDescriptor;
|
|
import java.io.File;
|
|
import java.io.FilenameFilter;
|
|
import java.io.IOException;
|
|
import java.lang.reflect.AnnotatedElement;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.ParameterizedType;
|
|
import java.lang.reflect.Type;
|
|
import java.nio.file.Files;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.World;
|
|
import org.bukkit.configuration.MemorySection;
|
|
import org.bukkit.configuration.file.YamlConfiguration;
|
|
import org.bukkit.entity.EntityType;
|
|
import org.eclipse.jdt.annotation.NonNull;
|
|
import org.eclipse.jdt.annotation.Nullable;
|
|
|
|
import com.google.common.base.Enums;
|
|
|
|
import world.bentobox.bentobox.BentoBox;
|
|
import world.bentobox.bentobox.api.configuration.ConfigComment;
|
|
import world.bentobox.bentobox.api.configuration.ConfigEntry;
|
|
import world.bentobox.bentobox.api.configuration.StoreAt;
|
|
import world.bentobox.bentobox.api.localization.TextVariables;
|
|
import world.bentobox.bentobox.database.AbstractDatabaseHandler;
|
|
import world.bentobox.bentobox.database.DatabaseConnector;
|
|
import world.bentobox.bentobox.database.objects.DataObject;
|
|
import world.bentobox.bentobox.database.objects.adapters.Adapter;
|
|
import world.bentobox.bentobox.database.objects.adapters.AdapterInterface;
|
|
import world.bentobox.bentobox.util.Util;
|
|
|
|
/**
|
|
* Class that creates a list of <T>s filled with values from the corresponding
|
|
* database-table.
|
|
*
|
|
* @author tastybento
|
|
*
|
|
* @param <T> Handles flat files for Class <T>
|
|
*/
|
|
|
|
public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
|
|
|
|
private static final String YML = ".yml";
|
|
|
|
/**
|
|
* Flag to indicate if this is a config or a pure object database (difference is in comments and annotations)
|
|
*/
|
|
protected boolean configFlag;
|
|
|
|
/**
|
|
* Constructor
|
|
* @param plugin - plugin
|
|
* @param type - class to store in the database
|
|
* @param databaseConnector - the database credentials, in this case, just the YAML functions
|
|
*/
|
|
YamlDatabaseHandler(BentoBox plugin, Class<T> type, DatabaseConnector databaseConnector) {
|
|
super(plugin, type, databaseConnector);
|
|
}
|
|
|
|
/* (non-Javadoc)
|
|
* @see world.bentobox.bentobox.database.AbstractDatabaseHandler#loadObject(java.lang.String)
|
|
*/
|
|
@Override
|
|
public T loadObject(@NonNull String key) throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException {
|
|
// Objects are loaded from a folder named after the simple name of the class being stored
|
|
String path = DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName();
|
|
// This path and key can be overridden by the StoreAt annotation in the code
|
|
StoreAt storeAt = dataObject.getAnnotation(StoreAt.class);
|
|
if (storeAt != null) {
|
|
path = storeAt.path();
|
|
key = storeAt.filename();
|
|
}
|
|
// Load the YAML file at the location.
|
|
YamlConfiguration config = ((YamlDatabaseConnector)databaseConnector).loadYamlFile(path, key);
|
|
// Use the createObject method to turn a YAML config into an Java object
|
|
return createObject(config);
|
|
}
|
|
|
|
@Override
|
|
public boolean objectExists(String uniqueId) {
|
|
// Check if the uniqueId (key) exists in the file system
|
|
return databaseConnector.uniqueIdExists(dataObject.getSimpleName(), uniqueId);
|
|
}
|
|
|
|
/* (non-Javadoc)
|
|
* @see world.bentobox.bentobox.database.AbstractDatabaseHandler#loadObjects()
|
|
*/
|
|
@Override
|
|
public List<T> loadObjects() throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException {
|
|
// In this case, all the objects of a specific type are being loaded.
|
|
List<T> list = new ArrayList<>();
|
|
// Look for any files that end in .yml in the folder
|
|
FilenameFilter ymlFilter = (dir, name) -> name.toLowerCase(java.util.Locale.ENGLISH).endsWith(YML);
|
|
// The path is the simple name of the class
|
|
String path = dataObject.getSimpleName();
|
|
// The storeAt annotation may override the path
|
|
StoreAt storeAt = dataObject.getAnnotation(StoreAt.class);
|
|
if (storeAt != null) {
|
|
path = storeAt.path();
|
|
}
|
|
// The database folder name is in the plugin's data folder
|
|
File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME);
|
|
// The folder for the objects (tables in database terminology) is here
|
|
File tableFolder = new File(dataFolder, path);
|
|
if (!tableFolder.exists()) {
|
|
// Nothing there...
|
|
tableFolder.mkdirs();
|
|
}
|
|
// Load each object from the file system, filtered, non-null
|
|
for (File file: Objects.requireNonNull(tableFolder.listFiles(ymlFilter))) {
|
|
String fileName = file.getName();
|
|
if (storeAt != null) {
|
|
fileName = storeAt.filename();
|
|
}
|
|
YamlConfiguration config = ((YamlDatabaseConnector)databaseConnector).loadYamlFile(DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName(), fileName);
|
|
list.add(createObject(config));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Creates a list of <T>s filled with values from the provided YamlConfiguration
|
|
*
|
|
* @param config - YAML config file
|
|
*
|
|
* @return <T> filled with values
|
|
* @throws SecurityException security exception
|
|
* @throws NoSuchMethodException no such method
|
|
* @throws IllegalArgumentException illegal argument
|
|
*/
|
|
private T createObject(YamlConfiguration config) throws InstantiationException, IllegalAccessException, IntrospectionException, InvocationTargetException, ClassNotFoundException, NoSuchMethodException {
|
|
// Create a new instance of the dataObject of type T (which can be any class)
|
|
T instance = dataObject.getDeclaredConstructor().newInstance();
|
|
|
|
// Run through all the fields in the object
|
|
for (Field field : dataObject.getDeclaredFields()) {
|
|
// Ignore synthetic fields, such as those added by Jacoco or the compiler
|
|
if (field.isSynthetic()) {
|
|
continue;
|
|
}
|
|
// Get the getter and setters for this field using the JavaBeans system
|
|
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(field.getName(), dataObject);
|
|
// Get the write method
|
|
Method method = propertyDescriptor.getWriteMethod();
|
|
/*
|
|
* Field annotation checks
|
|
*/
|
|
// Check if there is a ConfigEntry annotation on the field
|
|
ConfigEntry configEntry = field.getAnnotation(ConfigEntry.class);
|
|
|
|
// Determine the storage location
|
|
String storageLocation = (configEntry != null && !configEntry.path().isEmpty()) ? configEntry.path() : field.getName();
|
|
|
|
// Some fields need custom handling to serialize or deserialize and the programmer will need to
|
|
// define them herself. She can add an annotation to do that.
|
|
Adapter adapterNotation = field.getAnnotation(Adapter.class);
|
|
if (adapterNotation != null && AdapterInterface.class.isAssignableFrom(adapterNotation.value())) {
|
|
// A conversion adapter has been defined
|
|
// Get the original value to be stored
|
|
Object value = config.get(storageLocation);
|
|
// Invoke the deserialization on this value
|
|
method.invoke(instance, ((AdapterInterface<?,?>)adapterNotation.value().getDeclaredConstructor().newInstance()).deserialize(value));
|
|
// We are done here. If a custom adapter was defined, the rest of this method does not need to be run
|
|
} else if (config.contains(storageLocation)) { // Look in the YAML Config to see if this field exists (it should)
|
|
/*
|
|
* What follows is general deserialization code
|
|
*/
|
|
if (config.get(storageLocation) == null) { // Check for null values
|
|
method.invoke(instance, (Object)null);
|
|
} else if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
|
|
// Maps
|
|
deserializeMap(method, instance, storageLocation, config);
|
|
} else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
|
|
// Sets
|
|
deserializeSet(method, instance, storageLocation, config);
|
|
} else if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
|
|
// Lists
|
|
deserializeLists(method, instance, storageLocation, config);
|
|
} else {
|
|
// Non-collections
|
|
deserializeValue(method, instance, propertyDescriptor, storageLocation, config);
|
|
}
|
|
}
|
|
}
|
|
// After deserialization is complete, return the instance of the class we have created
|
|
return instance;
|
|
}
|
|
|
|
private void deserializeValue(Method method, T instance, PropertyDescriptor propertyDescriptor, String storageLocation, YamlConfiguration config) throws IllegalAccessException, InvocationTargetException {
|
|
// Not a collection. Get the value and rely on YAML to supply it
|
|
Object value = config.get(storageLocation);
|
|
// If the value is a yml MemorySection then something is wrong, so ignore it. Maybe an admin did some bad editing
|
|
if (value != null && !value.getClass().equals(MemorySection.class)) {
|
|
Object setTo = deserialize(value,propertyDescriptor.getPropertyType());
|
|
if (!(Enum.class.isAssignableFrom(propertyDescriptor.getPropertyType()) && setTo == null)) {
|
|
// Do not invoke null on Enums
|
|
try {
|
|
// Floats need special handling because the database returns them as doubles
|
|
Type setType = propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0];
|
|
if (setType.getTypeName().equals("float")) {
|
|
double d = (double) setTo;
|
|
float f = (float)d;
|
|
method.invoke(instance, f);
|
|
} else {
|
|
method.invoke(instance, setTo);
|
|
}
|
|
} catch (Exception e) {
|
|
plugin.logError("Could not deserialize. Attempt by " + instance.getClass().getCanonicalName() + " " + method.getName() + " to set to " + setTo);
|
|
plugin.logError("Error message is: " + e.getMessage());
|
|
plugin.logStacktrace(e);
|
|
}
|
|
} else {
|
|
plugin.logError("Default setting value will be used: " + propertyDescriptor.getReadMethod().invoke(instance));
|
|
plugin.logError(method.getName());
|
|
plugin.logError(propertyDescriptor.getReadMethod().getName());
|
|
plugin.logError(instance.toString());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void deserializeLists(Method method, T instance, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException {
|
|
// Note that we have no idea what type of List this is
|
|
List<Type> collectionTypes = getCollectionParameterTypes(method);
|
|
// collectionTypes should be only 1 long
|
|
Type setType = collectionTypes.get(0);
|
|
// Create an empty list
|
|
List<Object> value = new ArrayList<>();
|
|
// Lists are stored as lists in YAML
|
|
if (config.getList(storageLocation) != null) {
|
|
for (Object listValue: config.getList(storageLocation)) {
|
|
value.add(deserialize(listValue,Class.forName(setType.getTypeName())));
|
|
}
|
|
}
|
|
// Store the list using the setting
|
|
method.invoke(instance, value);
|
|
}
|
|
|
|
private void deserializeSet(Method method, T instance, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException {
|
|
// Note that we have no idea what type this set is
|
|
List<Type> collectionTypes = getCollectionParameterTypes(method);
|
|
// collectionTypes should be only 1 long
|
|
Type setType = collectionTypes.get(0);
|
|
// Create an empty set to fill
|
|
Set<Object> value = new HashSet<>();
|
|
// Sets are stored as a list in YAML
|
|
if (config.getList(storageLocation) != null) {
|
|
for (Object listValue: config.getList(storageLocation)) {
|
|
value.add(deserialize(listValue,Class.forName(setType.getTypeName())));
|
|
}
|
|
}
|
|
// Store the set using the setter in the class
|
|
method.invoke(instance, value);
|
|
}
|
|
|
|
private void deserializeMap(Method method, T instance, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException {
|
|
// Note that we have no idea what type of map this is, so we need to find out
|
|
List<Type> collectionTypes = getCollectionParameterTypes(method);
|
|
// collectionTypes should be 2 long because there are two parameters in a Map (key, value)
|
|
Type keyType = collectionTypes.get(0);
|
|
Type valueType = collectionTypes.get(1);
|
|
// Create a map that we'll put the values into
|
|
Map<Object,Object> value = new HashMap<>();
|
|
// Map values are stored in a configuration section in the YAML. Check that it exists
|
|
if (config.getConfigurationSection(storageLocation) != null) {
|
|
// Run through the values stored
|
|
for (String key : config.getConfigurationSection(storageLocation).getKeys(false)) {
|
|
// Map values can be null - it is allowed here
|
|
Object mapValue = deserialize(config.get(storageLocation + "." + key), Class.forName(valueType.getTypeName()));
|
|
// Keys cannot be null - skip if they exist
|
|
// Convert any serialized dots back to dots
|
|
// In YAML dots . cause a lot of problems, so I serialize them as :dot:
|
|
// There may be a better way to do this.
|
|
key = key.replace(":dot:", ".");
|
|
Object mapKey = deserialize(key,Class.forName(keyType.getTypeName()));
|
|
if (mapKey == null) {
|
|
continue;
|
|
}
|
|
// Put the value in the map
|
|
value.put(mapKey, mapValue);
|
|
}
|
|
}
|
|
// Invoke the setter in the class (this is why JavaBeans requires getters and setters for every field)
|
|
method.invoke(instance, value);
|
|
}
|
|
|
|
/**
|
|
* Get a list of parameter types for the collection argument in this method
|
|
* @param writeMethod - write method
|
|
* @return a list of parameter types for the collection argument in this method
|
|
*/
|
|
private List<Type> getCollectionParameterTypes(Method writeMethod) {
|
|
List<Type> result = new ArrayList<>();
|
|
// Get the return type
|
|
// This uses a trick to extract what the arguments are of the writeMethod of the field.
|
|
// In this way, we can deduce what type needs to be written at runtime.
|
|
Type[] genericParameterTypes = writeMethod.getGenericParameterTypes();
|
|
// There could be more than one argument, so step through them
|
|
for (Type genericParameterType : genericParameterTypes) {
|
|
// If the argument is a parameter, then do something - this should always be true if the parameter is a collection
|
|
if(genericParameterType instanceof ParameterizedType pt) {
|
|
// Get the actual type arguments of the parameter
|
|
Type[] parameters = pt.getActualTypeArguments();
|
|
result.addAll(Arrays.asList(parameters));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Inserts T into the corresponding database-table
|
|
*
|
|
* @param instance that should be inserted into the database
|
|
* @return CompletableFuture that will be true if object is saved successfully
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
@Override
|
|
public CompletableFuture<Boolean> saveObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException {
|
|
CompletableFuture<Boolean> completableFuture = new CompletableFuture<>();
|
|
// Null check
|
|
if (instance == null) {
|
|
plugin.logError("YAML database request to store a null.");
|
|
return CompletableFuture.completedFuture(false);
|
|
}
|
|
if (!(instance instanceof DataObject)) {
|
|
plugin.logError("This class is not a DataObject: " + instance.getClass().getName());
|
|
return CompletableFuture.completedFuture(false);
|
|
}
|
|
// This is the Yaml Configuration that will be used and saved at the end
|
|
YamlConfiguration config = new YamlConfiguration();
|
|
|
|
// Comments for the file
|
|
Map<String, String> yamlComments = new HashMap<>();
|
|
|
|
// Only allow storing in an arbitrary place if it is a config object. Otherwise it is in the database
|
|
StoreAt storeAt = instance.getClass().getAnnotation(StoreAt.class);
|
|
String path = storeAt == null ? DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName() : storeAt.path();
|
|
String filename = storeAt == null ? "" : storeAt.filename();
|
|
|
|
// See if there are any top-level comments
|
|
handleComments(instance.getClass(), config, yamlComments, "");
|
|
|
|
// Run through all the fields in the class that is being stored. EVERY field must have a get and set method
|
|
for (Field field : dataObject.getDeclaredFields()) {
|
|
if (field.isSynthetic()) {
|
|
continue;
|
|
}
|
|
// Get the property descriptor for this field
|
|
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(field.getName(), dataObject);
|
|
// Get the read method
|
|
Method method = propertyDescriptor.getReadMethod();
|
|
// Invoke the read method to get the value. We have no idea what type of value it is.
|
|
Object value = method.invoke(instance);
|
|
|
|
String storageLocation = field.getName();
|
|
|
|
// Check if there is an annotation on the field
|
|
ConfigEntry configEntry = field.getAnnotation(ConfigEntry.class);
|
|
|
|
// If there is a config path annotation or adapter then deal with them
|
|
if (configEntry != null && !configEntry.path().isEmpty()) {
|
|
if (configEntry.hidden()) {
|
|
// If the annotation tells us to not print the config entry, then we won't.
|
|
continue;
|
|
}
|
|
|
|
// Get the storage location
|
|
storageLocation = configEntry.path();
|
|
|
|
// Get path for comments
|
|
String parent = "";
|
|
if (storageLocation.contains(".")) {
|
|
parent = storageLocation.substring(0, storageLocation.lastIndexOf('.')) + ".";
|
|
}
|
|
handleComments(field, config, yamlComments, parent);
|
|
handleConfigEntryComments(configEntry, config, yamlComments, parent);
|
|
}
|
|
|
|
if (!checkAdapter(field, config, storageLocation, value)) {
|
|
// Set the filename if it has not be set already
|
|
if (filename.isEmpty() && method.getName().equals("getUniqueId")) {
|
|
// Save the name for when the file is saved
|
|
filename = getFilename(propertyDescriptor, instance, (String)value);
|
|
}
|
|
// Collections need special serialization
|
|
if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType()) && value != null) {
|
|
serializeMap((Map<Object,Object>)value, config, storageLocation);
|
|
} else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType()) && value != null) {
|
|
serializeSet((Set<Object>)value, config, storageLocation);
|
|
} else {
|
|
// For all other data that doesn't need special serialization
|
|
config.set(storageLocation, serialize(value));
|
|
}
|
|
}
|
|
}
|
|
// If the filename has not been set by now then we have a problem
|
|
if (filename.isEmpty()) {
|
|
throw new IllegalArgumentException("No uniqueId in class");
|
|
}
|
|
|
|
// Save
|
|
save(completableFuture, filename, config.saveToString(), path, yamlComments);
|
|
return completableFuture;
|
|
}
|
|
|
|
private void save(CompletableFuture<Boolean> completableFuture, String name, String data, String path, Map<String, String> yamlComments) {
|
|
if (plugin.isEnabled()) {
|
|
// Async
|
|
processQueue.add(() -> completableFuture.complete(
|
|
((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments)));
|
|
} else {
|
|
// Sync for shutdown
|
|
completableFuture.complete(
|
|
((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments));
|
|
}
|
|
}
|
|
|
|
private void serializeSet(Set<Object> value, YamlConfiguration config, String storageLocation) {
|
|
// Sets need to be serialized as string lists
|
|
List<Object> list = new ArrayList<>();
|
|
for (Object object : value) {
|
|
list.add(serialize(object));
|
|
}
|
|
// Save the list in the config file
|
|
config.set(storageLocation, list);
|
|
}
|
|
|
|
private void serializeMap(Map<Object, Object> value, YamlConfiguration config, String storageLocation) {
|
|
// Maps need to have keys serialized
|
|
Map<Object, Object> result = new HashMap<>();
|
|
for (Entry<Object, Object> object : value.entrySet()) {
|
|
// Serialize all key and values
|
|
String key = (String)serialize(object.getKey());
|
|
key = key.replace("\\.", ":dot:");
|
|
result.put(key, serialize(object.getValue()));
|
|
}
|
|
// Save the list in the config file
|
|
config.set(storageLocation, result);
|
|
}
|
|
|
|
private String getFilename(PropertyDescriptor propertyDescriptor, T instance, String id) throws IllegalAccessException, InvocationTargetException {
|
|
// If the object does not have a unique name assigned to it already, one is created at random
|
|
if (id == null || id.isEmpty()) {
|
|
id = databaseConnector.getUniqueId(dataObject.getSimpleName());
|
|
// Set it in the class so that it will be used next time
|
|
propertyDescriptor.getWriteMethod().invoke(instance, id);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Checks if an adapter is to be used. If so, it is used and true returned, if not, fase is returned
|
|
* @param field Field
|
|
* @param config Yaml Config
|
|
* @param storageLocation Storage location
|
|
* @param value Value
|
|
* @return true if adapater used
|
|
* @throws IllegalAccessException exception
|
|
* @throws InvocationTargetException exception
|
|
*/
|
|
private boolean checkAdapter(Field field, YamlConfiguration config, String storageLocation, Object value) throws IllegalAccessException, InvocationTargetException {
|
|
Adapter adapterNotation = field.getAnnotation(Adapter.class);
|
|
if (adapterNotation != null && AdapterInterface.class.isAssignableFrom(adapterNotation.value())) {
|
|
// A conversion adapter has been defined
|
|
try {
|
|
config.set(storageLocation, ((AdapterInterface<?,?>)adapterNotation.value().getDeclaredConstructor().newInstance()).serialize(value));
|
|
} catch (InstantiationException | IllegalArgumentException | NoSuchMethodException | SecurityException e) {
|
|
plugin.logError("Could not instantiate adapter " + adapterNotation.value().getName() + " " + e.getMessage());
|
|
}
|
|
// We are done here
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handles comments that are set on a Field or a Class using the {@link ConfigComment} annotation.
|
|
* @since 1.3.0
|
|
*/
|
|
private void handleComments(@NonNull AnnotatedElement annotatedElement, @NonNull YamlConfiguration config, @NonNull Map<String, String> yamlComments, @NonNull String parent) {
|
|
// See if there are multiple comments
|
|
ConfigComment.Line comments = annotatedElement.getAnnotation(ConfigComment.Line.class);
|
|
if (comments != null) {
|
|
for (ConfigComment comment : comments.value()) {
|
|
setComment(comment.value(), config, yamlComments, parent);
|
|
}
|
|
}
|
|
// Handle single line comments
|
|
ConfigComment comment = annotatedElement.getAnnotation(ConfigComment.class);
|
|
if (comment != null) {
|
|
setComment(comment.value(), config, yamlComments, parent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles comments that should be added according to the values set in the {@link ConfigEntry} annotation of a Field.
|
|
* @since 1.3.0
|
|
*/
|
|
private void handleConfigEntryComments(@NonNull ConfigEntry configEntry, @NonNull YamlConfiguration config, @NonNull Map<String, String> yamlComments, @NonNull String parent) {
|
|
// Tell if there is a video associated to this configuration option.
|
|
if (!configEntry.video().isEmpty()) {
|
|
setComment("You can find more details in this video: " + configEntry.video(), config, yamlComments, parent);
|
|
}
|
|
|
|
// Tell when the configEntry has been added (if it's not "1.0")
|
|
if (!configEntry.since().equals("1.0")) {
|
|
setComment("Added since " + configEntry.since() + ".", config, yamlComments, parent);
|
|
}
|
|
|
|
// Tell if the configEntry is experimental
|
|
if (configEntry.experimental()) {
|
|
setComment("/!\\ This feature is experimental and might not work as expected or might not work at all.", config, yamlComments, parent);
|
|
}
|
|
|
|
// Tell if the configEntry needs a reset.
|
|
if (configEntry.needsReset()) {
|
|
setComment("/!\\ BentoBox currently does not support changing this value mid-game. If you do need to change it, do a full reset of your databases and worlds.", config, yamlComments, parent);
|
|
}
|
|
// Tell if the configEntry needs the server to be restarted.
|
|
if (configEntry.needsRestart()) {
|
|
setComment("/!\\ In order to apply the changes made to this option, you must restart your server. Reloading BentoBox or the server won't work.", config, yamlComments, parent);
|
|
}
|
|
}
|
|
|
|
private void setComment(@NonNull String comment, @NonNull YamlConfiguration config, @NonNull Map<String, String> yamlComments, @NonNull String parent) {
|
|
String random = "comment-" + UUID.randomUUID();
|
|
// Store placeholder
|
|
config.set(parent + random, " ");
|
|
// Create comment
|
|
yamlComments.put(random, "# " + comment.replace(TextVariables.VERSION, Objects.isNull(getAddon()) ? plugin.getDescription().getVersion() : getAddon().getDescription().getVersion()));
|
|
}
|
|
|
|
/**
|
|
* Serialize an object if required. This means that an object will be turned into text to store in YAML
|
|
* @param object - object to serialize
|
|
* @return - serialized object
|
|
*/
|
|
@NonNull
|
|
private Object serialize(@Nullable Object object) {
|
|
// Null is a value object and is serialized as the string "null"
|
|
if (object == null) {
|
|
return "null";
|
|
}
|
|
// UUID has it's own serialization, that is not picked up automatically
|
|
if (object instanceof UUID) {
|
|
return object.toString();
|
|
}
|
|
// Only the world name is needed for worlds
|
|
if (object instanceof World w) {
|
|
return w.getName();
|
|
}
|
|
// Location
|
|
if (object instanceof Location l) {
|
|
return Util.getStringLocation(l);
|
|
}
|
|
// Enums
|
|
if (object instanceof Enum<?> e) {
|
|
//Custom enums are a child of the Enum class. Just get the names of each one.
|
|
return e.name();
|
|
}
|
|
return object;
|
|
}
|
|
|
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
|
@Nullable
|
|
private Object deserialize(Object value, Class<?> clazz) {
|
|
// If value is already null, then it can be nothing else
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (value instanceof String && value.equals("null")) {
|
|
// If the value is null as a string, return null
|
|
return null;
|
|
}
|
|
// Bukkit may have deserialized the object already
|
|
if (clazz.equals(value.getClass())) {
|
|
return value;
|
|
}
|
|
// Types that need to be deserialized
|
|
if (clazz.equals(Long.class) && value.getClass().equals(Integer.class)) {
|
|
return Long.valueOf((Integer) value);
|
|
}
|
|
if (value.getClass().equals(String.class)) {
|
|
if (clazz.equals(Integer.class)) {
|
|
return Integer.valueOf((String) value);
|
|
}
|
|
if (clazz.equals(Long.class)) {
|
|
return Long.valueOf((String) value);
|
|
}
|
|
if (clazz.equals(Double.class)) {
|
|
return Double.valueOf((String) value);
|
|
}
|
|
if (clazz.equals(Float.class)) {
|
|
return Float.valueOf((String) value);
|
|
}
|
|
}
|
|
if (clazz.equals(UUID.class)) {
|
|
value = UUID.fromString((String)value);
|
|
}
|
|
// Bukkit Types
|
|
if (clazz.equals(Location.class)) {
|
|
// Get Location from String - may be null...
|
|
value = Util.getLocationString(((String)value));
|
|
}
|
|
if (clazz.equals(World.class)) {
|
|
// Get world by name - may be null...
|
|
value = Bukkit.getWorld((String)value);
|
|
}
|
|
// Enums
|
|
if (Enum.class.isAssignableFrom(clazz)) {
|
|
//Custom enums are a child of the Enum class.
|
|
// Find out the value
|
|
Class<Enum> enumClass = (Class<Enum>)clazz;
|
|
try {
|
|
String name = ((String)value).toUpperCase(Locale.ENGLISH);
|
|
// Backwards compatibility for upgrade to 1.16.1
|
|
if (name.equals("PIG_ZOMBIE") || name.equals("ZOMBIFIED_PIGLIN")) {
|
|
return Enums.getIfPresent(EntityType.class, "ZOMBIFIED_PIGLIN")
|
|
.or(Enums.getIfPresent(EntityType.class, "PIG_ZOMBIE").or(EntityType.PIG));
|
|
}
|
|
// Backwards compatibility for upgrade to 1.20.4
|
|
if (name.equals("GRASS")) {
|
|
return Enums.getIfPresent(EntityType.class, "SHORT_GRASS");
|
|
}
|
|
value = Enum.valueOf(enumClass, name);
|
|
} catch (Exception e) {
|
|
// This value does not exist - probably admin typed it wrongly
|
|
// Show what is available and pick one at random
|
|
plugin.logError("Error in YML file: " + value + " is not a valid value in the enum " + clazz.getCanonicalName() + "!");
|
|
plugin.logError("Options are : ");
|
|
for (Field fields : enumClass.getFields()) {
|
|
plugin.logError(fields.getName());
|
|
}
|
|
value = null;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
@Override
|
|
public void deleteID(String uniqueId) {
|
|
if (plugin.isEnabled()) {
|
|
processQueue.add(() -> delete(uniqueId));
|
|
} else {
|
|
delete(uniqueId);
|
|
}
|
|
}
|
|
|
|
private void delete(String uniqueId) {
|
|
if (uniqueId == null) {
|
|
return;
|
|
}
|
|
// The filename of the YAML file is the value of uniqueId field plus .yml. Sometimes the .yml is already appended.
|
|
if (!uniqueId.endsWith(YML)) {
|
|
uniqueId = uniqueId + YML;
|
|
}
|
|
// Get the database and table folders
|
|
File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME);
|
|
File tableFolder = new File(dataFolder, dataObject.getSimpleName());
|
|
if (tableFolder.exists()) {
|
|
// Obtain the file and delete it
|
|
File file = new File(tableFolder, uniqueId);
|
|
try {
|
|
Files.deleteIfExists(file.toPath());
|
|
} catch (IOException e) {
|
|
plugin.logError("Could not delete yml database object! " + file.getName() + " - " + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/* (non-Javadoc)
|
|
* @see world.bentobox.bentobox.database.AbstractDatabaseHandler#deleteObject(java.lang.Object)
|
|
*/
|
|
@Override
|
|
public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException {
|
|
// Null check
|
|
if (instance == null) {
|
|
plugin.logError("YAML database request to delete a null.");
|
|
return;
|
|
}
|
|
if (!(instance instanceof DataObject)) {
|
|
plugin.logError("This class is not a DataObject: " + instance.getClass().getName());
|
|
return;
|
|
}
|
|
|
|
// Obtain the value of uniqueId within the instance (which must be a DataObject)
|
|
PropertyDescriptor propertyDescriptor = new PropertyDescriptor("uniqueId", dataObject);
|
|
Method method = propertyDescriptor.getReadMethod();
|
|
deleteID((String) method.invoke(instance));
|
|
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
// Not used
|
|
}
|
|
}
|