diff --git a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java index 43d4aefc8..7e6e81855 100644 --- a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java @@ -31,9 +31,9 @@ import org.bukkit.World; import org.bukkit.configuration.MemorySection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.scheduler.BukkitTask; - import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.configuration.ConfigComment; import world.bentobox.bentobox.api.configuration.ConfigEntry; @@ -186,23 +186,23 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // Run through all the fields in the object for (Field field : dataObject.getDeclaredFields()) { - // Gets the getter and setters for this field using the JavaBeans system + // 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(); - - // Information about the field - String storageLocation = field.getName(); /* * Field annotation checks */ // Check if there is a ConfigEntry annotation on the field ConfigEntry configEntry = field.getAnnotation(ConfigEntry.class); - // If there is a config annotation then do something - if (configEntry != null && !configEntry.path().isEmpty()) { - storageLocation = configEntry.path(); - } + // 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); @@ -213,91 +213,24 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // 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 - continue; - } - /* - * What follows is general deserialization code - */ - // Look in the YAML Config to see if this field exists (it should) - if (config.contains(storageLocation)) { - // Check for null values - if (config.get(storageLocation) == null) { + } 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); - continue; - } - // Handle storage of maps. Check if this field type is a Map - if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { - // Note that we have no idea what type of map this is, so we need to find out - List 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 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.replaceAll(":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); + } else if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { + // Maps + deserializeMap(method, instance, propertyDescriptor, storageLocation, config); } else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { - // Note that we have no idea what type this set is - List collectionTypes = getCollectionParameterTypes(method); - // collectionTypes should be only 1 long - Type setType = collectionTypes.get(0); - // Create an empty set to fill - Set 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); + // Sets + deserializeSet(method, instance, propertyDescriptor, storageLocation, config); } else if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { - // Note that we have no idea what type of List this is - List collectionTypes = getCollectionParameterTypes(method); - // collectionTypes should be only 1 long - Type setType = collectionTypes.get(0); - // Create an empty list - List 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); + // Lists + deserializeLists(method, instance, propertyDescriptor, storageLocation, config); } else { - // 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 - method.invoke(instance, setTo); - } else { - plugin.logError("Default setting value will be used: " + propertyDescriptor.getReadMethod().invoke(instance)); - } - } + // Non-collections + deserializeValue(method, instance, propertyDescriptor, storageLocation, config); } } } @@ -305,6 +238,87 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { return instance; } + private void deserializeValue(Method method, T instance, PropertyDescriptor propertyDescriptor, String storageLocation, + YamlConfiguration config) throws IllegalAccessException, IllegalArgumentException, 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 + method.invoke(instance, setTo); + } else { + plugin.logError("Default setting value will be used: " + propertyDescriptor.getReadMethod().invoke(instance)); + } + } + } + + private void deserializeLists(Method method, T instance, PropertyDescriptor propertyDescriptor, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + // Note that we have no idea what type of List this is + List collectionTypes = getCollectionParameterTypes(method); + // collectionTypes should be only 1 long + Type setType = collectionTypes.get(0); + // Create an empty list + List 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, PropertyDescriptor propertyDescriptor, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + // Note that we have no idea what type this set is + List collectionTypes = getCollectionParameterTypes(method); + // collectionTypes should be only 1 long + Type setType = collectionTypes.get(0); + // Create an empty set to fill + Set 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, PropertyDescriptor propertyDescriptor, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + // Note that we have no idea what type of map this is, so we need to find out + List 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 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.replaceAll(":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 @@ -319,7 +333,7 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // 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 ) { + if(genericParameterType instanceof ParameterizedType ) { // Get the actual type arguments of the parameter Type[] parameters = ((ParameterizedType)genericParameterType).getActualTypeArguments(); result.addAll(Arrays.asList(parameters)); @@ -362,6 +376,9 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // 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 @@ -374,7 +391,7 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // Check if there is an annotation on the field ConfigEntry configEntry = field.getAnnotation(ConfigEntry.class); - // If there is a config path annotation then do something + // 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. @@ -393,70 +410,34 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { handleConfigEntryComments(configEntry, config, yamlComments, parent); } - // Adapter - 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 + if (checkAdapter(field, config, storageLocation, value)) { continue; } - - // Depending on the value type, it'll need serializing differently - // Check if this field is the mandatory UniqueId field. This is used to identify this instantiation of the class - if (method.getName().equals("getUniqueId")) { - // If the object does not have a unique name assigned to it already, one is created at random - String id = (String)value; - if (value == 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); - } + // 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 - if (filename.isEmpty()) { - filename = id; - } + filename = getFilename(field, propertyDescriptor, instance, (String)value); } // Collections need special serialization - if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { - // Maps need to have keys serialized - if (value != null) { - Map result = new HashMap<>(); - for (Entry object : ((Map)value).entrySet()) { - // Serialize all key and values - String key = (String)serialize(object.getKey()); - key = key.replaceAll("\\.", ":dot:"); - result.put(key, serialize(object.getValue())); - } - // Save the list in the config file - config.set(storageLocation, result); - } - } else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { - // Sets need to be serialized as string lists - if (value != null) { - List list = new ArrayList<>(); - for (Object object : (Set)value) { - list.add(serialize(object)); - } - // Save the list in the config file - config.set(storageLocation, list); - } + if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType()) && value != null) { + serializeMap((Map)value, config, storageLocation); + } else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType()) && value != null) { + serializeSet((Set)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 - String name = filename; - String data = config.saveToString(); + save(filename, config.saveToString(), path, yamlComments); + } + + private void save(String name, String data, String path, Map yamlComments) { if (plugin.isEnabled()) { // Async saveQueue.add(() -> ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments)); @@ -466,6 +447,54 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { } } + private void serializeSet(Set value, YamlConfiguration config, String storageLocation) { + // Sets need to be serialized as string lists + List 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 value, YamlConfiguration config, String storageLocation) { + // Maps need to have keys serialized + Map result = new HashMap<>(); + for (Entry object : value.entrySet()) { + // Serialize all key and values + String key = (String)serialize(object.getKey()); + key = key.replaceAll("\\.", ":dot:"); + result.put(key, serialize(object.getValue())); + } + // Save the list in the config file + config.set(storageLocation, result); + } + + private String getFilename(Field field, PropertyDescriptor propertyDescriptor, T instance, String id) throws IllegalAccessException, IllegalArgumentException, 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; + } + + 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