Skip to content

Commit

Permalink
Added automatic serializer for custom classes based on reflections
Browse files Browse the repository at this point in the history
  • Loading branch information
mikigal committed Nov 1, 2021
1 parent b062d03 commit b27b346
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 5 deletions.
40 changes: 38 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Config API for Bukkit 1.8 - 1.17 based on dynamic proxies
- Automatic update of config's file after add new methods to it's interface
- Support for setting new values to config
- System of serializers for custom objects (e.g. ItemStack, Location)
- Automatic serialization of custom objects based on reflections (mainly for simple DAO/DTO objects)
- Support of comments in YAML config
- Automatic translation of `&` based colors

Expand All @@ -20,7 +21,7 @@ maven {
url = 'https://repo.mikigal.pl/releases'
}
compile group: 'pl.mikigal', name: 'ConfigAPI', version: '1.1.6'
compile group: 'pl.mikigal', name: 'ConfigAPI', version: '1.1.7'
```

#### Maven
Expand All @@ -33,7 +34,7 @@ compile group: 'pl.mikigal', name: 'ConfigAPI', version: '1.1.6'
<dependency>
<groupId>pl.mikigal</groupId>
<artifactId>ConfigAPI</artifactId>
<version>1.1.6</version>
<version>1.1.7</version>
<scope>compile</scope>
</dependency>
```
Expand Down Expand Up @@ -160,7 +161,41 @@ award:
- ShapedRecipe
- UUID
#### Automatic serializing of custom objects
If you have some simple DAO/DTO object you can serialize it without writing custom serializer!
```java
public class User implements Serializable { // It must implement Serializable interface
private String username;
private int kills;
private transient String temporary; // Transient fields will not be serialized!

public User() { // It MUST have no-args constructor!

}

public User(String username, int kills, String temporary) {
this.username = username;
this.kills = kills;
this.temporary = temporary;
}
// Getters and setters...
}

@ConfigName("config")
public interface MyConfig extends Config {
void setUser(User user);
default User getTest() {
return new User("mikigal", 1, "some text");
}
}
```

#### You can also make your own serializers
For more advanced objects you can make your own serializer
```java
public class PotionEffectSerializer extends Serializer<PotionEffect> {

Expand Down Expand Up @@ -198,3 +233,4 @@ public class TestPlugin extends JavaPlugin {

}
}

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group 'pl.mikigal'
version '1.1.6'
version '1.1.7'

publishing {
repositories {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/pl/mikigal/config/ConfigInvocationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ private boolean updateConfigFile() {
throw new InvalidConfigException("Found non getter/setter method (name: " + name + ") in " + clazz.getCanonicalName());
}

if (method.getParameters().length != 0) {
throw new InvalidConfigException("Found method with parameters (name: " + name + ") in " + clazz.getCanonicalName());
}

if (!method.isDefault() || this.configuration.contains(this.getConfigPath(method))) {
continue;
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/pl/mikigal/config/serializer/Serializers.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import pl.mikigal.config.serializer.universal.UniversalArraySerializer;
import pl.mikigal.config.serializer.universal.UniversalCollectionSerializer;
import pl.mikigal.config.serializer.universal.UniversalMapSerializer;
import pl.mikigal.config.serializer.universal.UniversalObjectSerializer;

import java.io.Serializable;
import java.util.*;

/**
Expand All @@ -22,7 +24,7 @@ public class Serializers {
/**
* Map of registered serializers
*/
public static final Map<Class<?>, Serializer<?>> SERIALIZERS = new HashMap<>();
public static final Map<Class<?>, Serializer<?>> SERIALIZERS = new LinkedHashMap<>();

static {
register(ItemStack.class, new ItemStackSerializer());
Expand All @@ -34,6 +36,7 @@ public class Serializers {
register(Object[].class, new UniversalArraySerializer());
register(Collection.class, new UniversalCollectionSerializer());
register(Map.class, new UniversalMapSerializer());
register(Serializable.class, new UniversalObjectSerializer());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package pl.mikigal.config.serializer.universal;

import pl.mikigal.config.BukkitConfiguration;
import pl.mikigal.config.serializer.Serializer;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
* Helper built-in serializer for custom objects which implemenet Serializable interface.
* It uses reflections to serialize all fields from given Object, which are not transient and static.
* Class must have default constructor (no-args).
* @see Serializer
* @see Serializable
* @since 1.1.7
* @author Mikołaj Gałązka
*/
public class UniversalObjectSerializer extends Serializer<Serializable> {

@Override
protected void saveObject(String path, Serializable object, BukkitConfiguration configuration) {
this.validateDefaultConstructor(object);

try {
for (Field field : object.getClass().getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
continue;
}

field.setAccessible(true);
Object value = field.get(object);

try {
configuration.set(path + "." + configuration.getNameStyle().format(field.getName()), value);
} catch (Exception e) {
throw new RuntimeException("An error occurred while serializing field '" + field.getName() + "' from class '" + object.getClass().getName() + "'", e);
}
}

configuration.set(path + "." + configuration.getNameStyle().format("type"), object.getClass().getName());
} catch (IllegalAccessException e) {
throw new RuntimeException("An error occurred while serializing class '" + object.getClass().getName() + "'", e);
}
}

@Override
public Serializable deserialize(String path, BukkitConfiguration configuration) {
String classPath = configuration.getString(path + "." + configuration.getNameStyle().format("type"));
Class<?> clazz;

try {
clazz = Class.forName(classPath);
} catch (ClassNotFoundException e) {
throw new RuntimeException("An error occurred while deserializing class '" + classPath + "'", e);
}

if (!Serializable.class.isAssignableFrom(clazz)) {
throw new RuntimeException("Class " + classPath + " does not implements Serializable");
}

Serializable instance;
try {
instance = (Serializable) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Could not create instance of class (" + classPath + ") with default constructor", e);
}

try {
for (Field field : clazz.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
continue;
}

field.setAccessible(true);
field.set(instance, configuration.get(path + "." + configuration.getNameStyle().format(field.getName())));
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Could not deserialize " + classPath, e);
}

return instance;
}

private void validateDefaultConstructor(Object object) {
try {
object.getClass().getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Class " + object.getClass().getName() + " does not have a default constructor");
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/pl/mikigal/config/style/NameStyle.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ public enum NameStyle {
* @return name of field in config
*/
public String format(String methodName) {
return CaseFormat.UPPER_CAMEL.to(this.caseFormat, methodName.replace("get", "").replace("set", ""));
return caseFormat.to(this.caseFormat, methodName.replace("get", "").replace("set", ""));
}
}

0 comments on commit b27b346

Please sign in to comment.