Skip synthetic fields from Jacoco or compiler

When using introspection, synthetic fields should be skipped. These
fields can be added by Jacoco or even the compiler so will cause
problems during unit testing.

Refactored YamlDatabaseHandler to be easier to understand.
This commit is contained in:
tastybento 2019-03-08 22:29:28 -08:00
parent d11a27dc6d
commit 9ba34d1e92

View File

@ -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<T> extends AbstractDatabaseHandler<T> {
// 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<T> extends AbstractDatabaseHandler<T> {
// 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<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.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<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);
// 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<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);
// 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<T> extends AbstractDatabaseHandler<T> {
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<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, PropertyDescriptor propertyDescriptor, String storageLocation, YamlConfiguration config) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, 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, 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<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.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<T> extends AbstractDatabaseHandler<T> {
// 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<T> extends AbstractDatabaseHandler<T> {
// 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<T> extends AbstractDatabaseHandler<T> {
// 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<T> extends AbstractDatabaseHandler<T> {
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<Object, Object> result = new HashMap<>();
for (Entry<Object, Object> object : ((Map<Object,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);
}
} else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
// Sets need to be serialized as string lists
if (value != null) {
List<Object> list = new ArrayList<>();
for (Object object : (Set<Object>)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<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
String name = filename;
String data = config.saveToString();
save(filename, config.saveToString(), path, yamlComments);
}
private void save(String name, String data, String path, Map<String, String> yamlComments) {
if (plugin.isEnabled()) {
// Async
saveQueue.add(() -> ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments));
@ -466,6 +447,54 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
}
}
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.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