Skip to content

Commit

Permalink
@AutoSerialize - automatic serializing and deserializing feature for …
Browse files Browse the repository at this point in the history
…ConfigSerializable
  • Loading branch information
Rubix327 committed Oct 21, 2022
1 parent c223999 commit 4e7c07e
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 61 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Please see the original for more information.
with AdvancedMenu, AdvancedMenuPagged and AdvancedMenuTools)
- Automatic YamlConfig loading (use @AutoConfig annotation
to load and save all of your fields to the file automatically)
- Automatic ConfigSerializable loading (use @AutoSerialize)
- Better CompMetadata - now you can store and remove temporary and PERSISTENT metadata for
all objects that support that regardless of your MC version.
- Some small but useful features (new Logger, ItemCreator attribute
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/mineacademy/fo/ReflectionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -1242,4 +1242,16 @@ public String getEnumName() {
return this.enumName;
}
}

/**
* Returns true if class is annotated OR field itself is annotated and enabled.
*/
public static boolean isAnnotationAttached(boolean overClass, boolean overField, boolean isEnabled){
if (overClass){
return !overField || isEnabled;
}
else{
return overField && isEnabled;
}
}
}
139 changes: 134 additions & 5 deletions src/main/java/org/mineacademy/fo/SerializeUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.mineacademy.fo;

import com.google.common.base.CaseFormat;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.NonNull;
Expand All @@ -19,6 +20,7 @@
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.mineacademy.fo.MinecraftVersion.V;
import org.mineacademy.fo.annotation.AutoSerialize;
import org.mineacademy.fo.collection.SerializedMap;
import org.mineacademy.fo.collection.StrictCollection;
import org.mineacademy.fo.collection.StrictMap;
Expand All @@ -34,9 +36,7 @@
import org.mineacademy.fo.settings.ConfigSection;

import java.awt.Color;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.util.List;
Expand Down Expand Up @@ -100,8 +100,12 @@ public static Object serialize(Mode mode, Object object) {
if (serializers.containsKey(object.getClass()))
return serializers.get(object.getClass()).apply(object);

if (object instanceof ConfigSerializable)
if (object instanceof ConfigSerializable){
if (object.getClass().isAnnotationPresent(AutoSerialize.class)){
return autoSerialize(object).serialize();
}
return serialize(mode, ((ConfigSerializable) object).serialize().serialize());
}

else if (object instanceof StrictCollection)
return serialize(mode, ((StrictCollection) object).serialize());
Expand Down Expand Up @@ -307,6 +311,55 @@ else if (object instanceof ConfigurationSerializable) {
throw new SerializeFailedException("Does not know how to serialize " + object.getClass().getSimpleName() + "! Does it implement ConfigSerializable? Data: " + object);
}

/**
* Set values annotated with @AutoSerialize from a ConfigSerializable class and save them to SerializedMap.
*/
public static SerializedMap autoSerialize(Object object){
SerializedMap map = new SerializedMap();
Class<?> classOf = object.getClass();

for (Field field : getFieldsToAutoSerialize(classOf)){
field.setAccessible(true);
try {
String name = getFormattedFieldName(classOf, field);
Object value = field.get(object);
if (value != null){
map.put(name, value);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return map;
}

public static List<Field> getFieldsToAutoSerialize(Class<?> classOf){
List<Field> fields = new ArrayList<>();
boolean isAboveClass = false;

// Do nothing if AutoSerialize is disabled for the whole class
if (classOf.isAnnotationPresent(AutoSerialize.class)){
isAboveClass = true;
if (!classOf.getAnnotation(AutoSerialize.class).value()){
return new ArrayList<>();
}
}

for (Field field : classOf.getDeclaredFields()){
boolean hasAnnotation = field.isAnnotationPresent(AutoSerialize.class);
boolean isEnabled = false;
if (hasAnnotation){
AutoSerialize ann = field.getAnnotation(AutoSerialize.class);
isEnabled = ann.value() && ann.autoSerialize();
}

if (Modifier.isStatic(field.getModifiers())) continue;
if (ReflectionUtil.isAnnotationAttached(isAboveClass, hasAnnotation, isEnabled)) fields.add(field);
}

return fields;
}

/*
* Helps to add an unknown element into a json list
*/
Expand Down Expand Up @@ -619,6 +672,11 @@ else if (List.class.isAssignableFrom(classOf) && object instanceof List) {

// Try to call our own serializers
else if (ConfigSerializable.class.isAssignableFrom(classOf)) {
SerializedMap map = isJson ? SerializedMap.fromJson(object.toString()) : SerializedMap.of(object);
if (classOf.isAnnotationPresent(AutoSerialize.class)){
return autoDeserialize(classOf, map);
}

if (parameters != null && parameters.length > 0) {
final List<Class<?>> argumentClasses = new ArrayList<>();
final List<Object> arguments = new ArrayList<>();
Expand All @@ -629,7 +687,7 @@ else if (ConfigSerializable.class.isAssignableFrom(classOf)) {
argumentClasses.add(param.getClass());

// Build parameter instances
arguments.add(isJson ? SerializedMap.fromJson(object.toString()) : SerializedMap.of(object));
arguments.add(map);
Collections.addAll(arguments, parameters);

// Find deserialize(SerializedMap, args[]) method
Expand Down Expand Up @@ -671,6 +729,77 @@ else if (classOf == Object.class) {
return (T) object;
}

/**
* Get values from SerializedMap and set them to fields annotated with @AutoSerialize
*/
public static <T> T autoDeserialize(Class<T> classOf, SerializedMap map){
Constructor<T> constructor;
try {
constructor = classOf.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Your class " + classOf.getSimpleName() + " must contain a no-arguments " +
"constructor because is annotated with @AutoSerialize.");
}
constructor.setAccessible(true);

T instance;
try {
instance = (T) constructor.newInstance();

for (Field field : getFieldsToAutoDeserialize(classOf)) {
field.setAccessible(true);
String name = getFormattedFieldName(classOf, field);
Object value = map.get(name, field.getType());
if (value != null){
field.set(instance, value);
}
}
}
catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
return instance;
}

public static List<Field> getFieldsToAutoDeserialize(Class<?> classOf){
List<Field> fields = new ArrayList<>();
boolean isAboveClass = false;

// Do nothing if AutoSerialize is disabled for the whole class
if (classOf.isAnnotationPresent(AutoSerialize.class)){
isAboveClass = true;
if (!classOf.getAnnotation(AutoSerialize.class).value()){
return new ArrayList<>();
}
}

for (Field field : classOf.getDeclaredFields()){
boolean hasAnnotation = field.isAnnotationPresent(AutoSerialize.class);
boolean isEnabled = false;
if (hasAnnotation){
AutoSerialize ann = field.getAnnotation(AutoSerialize.class);
isEnabled = ann.value() && ann.autoDeserialize();
}

if (Modifier.isStatic(field.getModifiers())) continue;
if (ReflectionUtil.isAnnotationAttached(isAboveClass, hasAnnotation, isEnabled)) fields.add(field);
}

return fields;
}

/**
* Get the name under which the value will be located in SerializedMap.
* Based on user-defined AutoSerialize.format() and is lower_underscore by default.
*/
private static String getFormattedFieldName(Class<?> classOf, Field field) {
String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName());
if (classOf.isAnnotationPresent(AutoSerialize.class)){
name = CaseFormat.LOWER_CAMEL.to(classOf.getAnnotation(AutoSerialize.class).format(), field.getName());
}
return name;
}

/**
* Converts a string into location, see {@link #deserializeLoc(Object)} for how strings are saved
* Decimals not supported, use {@link #deserializeLocD(Object)} to use them
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/mineacademy/fo/annotation/AutoConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@

/**
* When false, automatic loading does not work for class or field above which is set.<br>
* You should manually get the disabled fields in <i>onLoad</i> method.
* You may manually get the disabled fields in <i>onLoad</i> method if you want.
*/
boolean autoLoad() default true;

/**
* When false, automatic saving does not work for class or field above which is set.<br>
* You should manually set the disabled fields in <i>onSave</i> method.
* You may manually set the disabled fields in <i>onSave</i> method if you want.
*/
boolean autoSave() default true;

Expand Down
56 changes: 56 additions & 0 deletions src/main/java/org/mineacademy/fo/annotation/AutoSerialize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.mineacademy.fo.annotation;

import com.google.common.base.CaseFormat;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This annotation automatically serialized and deserializes fields for your
* custom class implementing {@link org.mineacademy.fo.model.ConfigSerializable}.<br><br>
* <b>On class</b>:
* <ul>
* <li>Serialized and deserialized all non-static class fields</li>
* <li>But skips fields that have disabled this feature by <i>@AutoSerialize(false)</i></li>
* </ul>
* When using on class, if you want to prevent one specific field from auto-serializing and auto-deserializing,
* use <i>@AutoSerialize(false)</i> on that field.<br>
* <br>
* <b>On field</b>:
* <ul>
* <li>Serialized and deserializes field if annotation is in enabled state</li>
* <li>Skips field serializing if <i>@AutoSerialize(autoSerialize = false)</i></li>
* <li>Skips field deserializing if <i>@AutoSerialize(autoDeserialize = false)</i></li>
* </ul>
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoSerialize {

/**
* When false, automatic serializing and deserializing does not work for class or field above which is set.
*/
boolean value() default true;

/**
* When false, automatic serializing does not work for class or field above which is set.<br>
* You may manually serialize the disabled fields in <i>serialize</i> method if you want.
*/
boolean autoSerialize() default true;

/**
* When false, automatic deserializing does not work for class or field above which is set.<br>
* You may manually set the disabled fields in <i>deserialize</i> method if you want.
*/
boolean autoDeserialize() default true;

/**
* In what format should we convert your fields to SerializedMap.<br>
* Only usable if set on class. You can only set one format for one class.<br>
* Default: lower_underscore.
*/
CaseFormat format() default CaseFormat.LOWER_UNDERSCORE;

}
55 changes: 28 additions & 27 deletions src/main/java/org/mineacademy/fo/collection/SerializedMap.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
package org.mineacademy.fo.collection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Function;

import lombok.Getter;
import lombok.NonNull;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.MemorySection;
import org.bukkit.inventory.ItemStack;
import org.mineacademy.fo.Common;
Expand All @@ -30,8 +20,10 @@
import org.mineacademy.fo.remain.Remain;
import org.mineacademy.fo.settings.ConfigSection;

import lombok.Getter;
import lombok.NonNull;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import java.util.function.Function;

/**
* Serialized map enables you to save and retain values from your
Expand Down Expand Up @@ -477,23 +469,32 @@ public Boolean getBoolean(final String key, final Boolean def) {
}

/**
* Returns a material from the map, or null if does not exist
*
* @param key
* @return
* Returns a material from the map, or null if it does not exist
*/
public CompMaterial getMaterial(final String key) {
return this.getMaterial(key, null);
public Material getMaterial(final String key){
return getMaterial(key, null);
}

/**
* Return a material from the map or the default given
*
* @param key
* @param def
* @return
* Returns a material from the map or the default given
*/
public Material getMaterial(final String key, final Material def){
final String raw = this.getString(key);

return raw != null ? Material.getMaterial(raw) : def;
}

/**
* Returns a CompMaterial from the map, or null if it does not exist
*/
public CompMaterial getCompMaterial(final String key) {
return this.getCompMaterial(key, null);
}

/**
* Returns a CompMaterial from the map or the default given
*/
public CompMaterial getMaterial(final String key, final CompMaterial def) {
public CompMaterial getCompMaterial(final String key, final CompMaterial def) {
final String raw = this.getString(key);

return raw != null ? CompMaterial.fromString(raw) : def;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ public interface ConfigSerializable {
/**
* Creates a Map representation of this class that you can
* save in your settings yaml or json file.
*<br><br>
* Since v6.2.1.5 we have @AutoSerialize so this method is not mandatory now.
*
* @return Map containing the current state of this class
*/
SerializedMap serialize();
default SerializedMap serialize() {
return null;
};
}
2 changes: 1 addition & 1 deletion src/main/java/org/mineacademy/fo/remain/CompMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ public static final class BlockCache implements ConfigSerializable {
private final List<String> metadata;

public static BlockCache deserialize(final SerializedMap map) {
final CompMaterial type = map.getMaterial("Type");
final CompMaterial type = map.getCompMaterial("Type");
final List<String> metadata = map.getStringList("Metadata");

return new BlockCache(type, metadata);
Expand Down
Loading

0 comments on commit 4e7c07e

Please sign in to comment.