1
mirror of https://github.com/CarmJos/EasyConfiguration.git synced 2026-06-04 10:38:19 +08:00

feat(adapter): Support complex ParameterizedType's serialize & deserialize.

This commit is contained in:
2025-06-25 09:31:07 +08:00
parent 608d92f834
commit 8c1214612a
23 changed files with 213 additions and 57 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
@@ -2,12 +2,13 @@ package cc.carm.lib.configuration.adapter;
import cc.carm.lib.configuration.function.DataFunction;
import cc.carm.lib.configuration.source.ConfigurationHolder;
import cc.carm.lib.configuration.source.section.ConfigureSection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
public class ValueAdapterRegistry {
@@ -108,20 +109,106 @@ public class ValueAdapterRegistry {
public <T> T deserialize(@NotNull ConfigurationHolder<?> holder, @NotNull ValueType<T> type, @Nullable Object source) throws Exception {
if (source == null) return null; // Null check
if (type.isInstance(source)) return type.cast(source); // Not required to deserialize
ValueAdapter<T> adapter = adapterOf(type);
if (adapter == null) throw new RuntimeException("No adapter for type " + type);
if (!(type.getType() instanceof ParameterizedType) && type.isInstance(source)) {
return type.cast(source); // Not required to deserialize
}
ValueAdapter<T> adapter = adapterOf(type); // Try to find an existed adapter for the type
if (adapter != null) {
return adapter.parse(holder, type, source);
} // If no adapter found, we will try to handle the type manually
if (type.getRawType().isArray()) { // For arrays.
List<?> list = deserializeList(holder, type, source);
Object[] array = (Object[]) java.lang.reflect.Array.newInstance(type.getRawType().getComponentType(), list.size());
for (int i = 0; i < list.size(); i++) {
array[i] = deserialize(holder, type.getRawType().getComponentType(), list.get(i));
}
return type.cast(array);
} else if (type.getType() instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type.getType();
Type rawType = pt.getRawType();
Type[] typeArgs = pt.getActualTypeArguments();
if (rawType == List.class || rawType == Collection.class || rawType == ArrayList.class) {
return type.cast(new ArrayList<>(deserializeList(holder, ValueType.of(typeArgs[0]), source)));
} else if (rawType == Set.class || rawType == HashSet.class) {
return type.cast(new HashSet<>(deserializeList(holder, ValueType.of(typeArgs[0]), source)));
} else if (rawType == Map.class || rawType == LinkedHashMap.class) {
Map<?, ?> map;
if (source instanceof Map<?, ?>) {
map = (Map<?, ?>) source;
} else if (source instanceof ConfigureSection) {
map = ((ConfigureSection) source).asMap();
} else {
throw new IllegalArgumentException("Cannot deserialize to Map from " + source.getClass());
}
Map<Object, Object> resultMap = new LinkedHashMap<>(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = deserialize(holder, ValueType.of(typeArgs[0]), entry.getKey());
Object value = deserialize(holder, ValueType.of(typeArgs[1]), entry.getValue());
resultMap.put(key, value);
}
return type.cast(resultMap);
}
}
throw new RuntimeException("No adapter for type " + type);
}
@Nullable
public <T> Object serialize(@NotNull ConfigurationHolder<?> holder, @Nullable T value) throws Exception {
if (value == null) return null; // Null check
ValueType<T> type = ValueType.of(value);
ValueAdapter<T> adapter = adapterOf(type);
if (adapter == null) return value; // No adapters, try to return the original value
return adapter.serialize(holder, type, value);
if (adapter != null) return adapter.serialize(holder, type, value);
if (value.getClass().isArray()) {
Object[] array = (Object[]) value;
List<Object> serializedList = new ArrayList<>(array.length);
for (Object item : array) {
serializedList.add(serialize(holder, item));
}
return serializedList;
} else if (value instanceof Collection<?>) {
Collection<?> collection = (Collection<?>) value;
List<Object> serializedList = new ArrayList<>(collection.size());
for (Object item : collection) {
serializedList.add(serialize(holder, item));
}
return serializedList;
} else if (value instanceof Map<?, ?>) {
Map<?, ?> map = (Map<?, ?>) value;
Map<Object, Object> serializedMap = new LinkedHashMap<>(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = serialize(holder, entry.getKey());
Object val = serialize(holder, entry.getValue());
serializedMap.put(key, val);
}
return serializedMap;
}
return value; // No adapters, and cannot handle, try to return the original value
}
protected <T> List<T> deserializeList(@NotNull ConfigurationHolder<?> holder,
@NotNull ValueType<T> type, @Nullable Object source) throws Exception {
if (source == null) return Collections.emptyList(); // Null check
if (source instanceof List<?>) {
List<?> list = (List<?>) source;
List<T> result = new ArrayList<>(list.size());
for (Object item : list) {
T deserializedItem = deserialize(holder, type, item);
if (deserializedItem != null) {
result.add(deserializedItem);
}
}
return result;
} else { // Maybe singleton? Let's try to deserialize it as a single element list
T deserializedItem = deserialize(holder, type, source);
if (deserializedItem != null) {
return Collections.singletonList(deserializedItem);
} else return Collections.emptyList();
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
@@ -14,7 +14,7 @@ import java.util.Map;
public class YamlTests {
@Test
public void test() {
public void test() throws Exception {
ConfigurationHolder<YAMLSource> holder = YAMLConfigFactory.from("target/tests.yml")
.resourcePath("configs/sample.yml").build();
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
@@ -38,10 +38,14 @@ public class RecordAdapter<T extends Record> extends ValueAdapter<T> {
return (holder, type1, r) -> toMap(holder, r);
}
@SuppressWarnings("unchecked")
public static <R extends Record> ValueParser<R> parser(@NotNull ValueType<R> type) {
return (holder, valueType, value) -> {
if (!(value instanceof ConfigureSection section)) return null;
return fromMap(holder, type.getRawType(), section.asMap());
if (value instanceof ConfigureSection section) {
return fromMap(holder, (Class<R>) valueType.getRawType(), section.asMap());
} else if (value instanceof Map<?, ?> map) {
return fromMap(holder, (Class<R>) valueType.getRawType(), (Map<String, Object>) map);
} else return null;
};
}
@@ -57,6 +61,7 @@ public class RecordAdapter<T extends Record> extends ValueAdapter<T> {
for (RecordComponent component : recordClass.getRecordComponents()) {
String name = component.getName();
Method accessor = component.getAccessor();
accessor.setAccessible(true);
Object value = accessor.invoke(record);
map.put(name, serializeValue(holder, value));
}
@@ -74,18 +79,19 @@ public class RecordAdapter<T extends Record> extends ValueAdapter<T> {
Object[] args = new Object[components.length];
for (int i = 0; i < components.length; i++) {
RecordComponent component = components[i];
args[i] = parseValue(holder, component.getType(), data.get(component.getName()));
args[i] = parseValue(holder, component, data.get(component.getName()));
}
return createInstance(type, args);
}
@SuppressWarnings("unchecked")
private static Object parseValue(ConfigurationHolder<?> holder, Class<?> targetType, Object value) throws Exception {
private static Object parseValue(ConfigurationHolder<?> holder, RecordComponent component, Object value) throws Exception {
if (value == null) return null;
if (targetType.isRecord()) {
return fromMap(holder, targetType.asSubclass(Record.class), (Map<String, Object>) value);
if (component.getType().isRecord()) {
return fromMap(holder, component.getType().asSubclass(Record.class), (Map<String, Object>) value);
}
return holder.deserialize(targetType, value);
ValueType<?> valueType = ValueType.of(component.getGenericType());
return holder.deserialize(valueType, value);
}
private static Object serializeValue(ConfigurationHolder<?> holder, Object value) throws Exception {
+54 -16
View File
@@ -1,57 +1,94 @@
import cc.carm.lib.configuration.Configuration;
import cc.carm.lib.configuration.annotation.ConfigPath;
import cc.carm.lib.configuration.source.ConfigurationHolder;
import cc.carm.lib.configuration.source.temp.TempConfigFactory;
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
import cc.carm.lib.configured.adapter.record.RecordAdapter;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class RecordTest {
interface Config extends Configuration {
@ConfigPath(root = true)
interface ConfigA extends Configuration {
ConfiguredValue<Device> VAL = ConfiguredValue.of(new Device(
"device1",
"My Device",
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
new Chip("chip1", "SN123456")
new Chip("chip1", "SN123456"),
Arrays.asList(
new User("Alice", 30),
new User("Bob", 25)
)
));
}
@ConfigPath(root = true)
interface ConfigB extends Configuration {
ConfiguredValue<Device> VAL = ConfiguredValue.of(Device.class);
}
@Test
public void test() {
ConfigurationHolder<?> holder = TempConfigFactory.create().build();
RecordAdapter.register(holder);
holder.initialize(Config.class);
holder.initialize(ConfigA.class);
System.out.println("Device ID: " + Config.VAL.resolve().id());
System.out.println("Device Name: " + Config.VAL.resolve().name());
System.out.println("Device Serial: " + Config.VAL.resolve().serial());
System.out.println("Chip ID: " + Config.VAL.resolve().chip().id());
System.out.println("Chip Serial: " + Config.VAL.resolve().chip().serialNumber());
try {
holder.save();
} catch (Exception e) {
throw new RuntimeException(e);
System.out.println("Device ID: " + ConfigA.VAL.resolve().id());
System.out.println("Device Name: " + ConfigA.VAL.resolve().name());
System.out.println("Device Serial: " + ConfigA.VAL.resolve().serial());
System.out.println("Chip ID: " + ConfigA.VAL.resolve().chip().id());
System.out.println("Chip Serial: " + ConfigA.VAL.resolve().chip().serialNumber());
for (User user : ConfigA.VAL.resolve().users()) {
System.out.println("Another Users: " + user.name() + ", Age: " + user.age());
}
printMap(holder.config().asMap(), 0);
// printMap(holder.config().asMap(), 0);
// try {
// List<User> parsed = holder.deserialize(ValueType.ofList(User.class), holder.config().getList("val.users"));
// System.out.println("Parsed Users: " + parsed);
// } catch (Exception e) {
// e.printStackTrace();
// }
ConfigurationHolder<?> anotherHolder = TempConfigFactory.create().defaults(() -> holder.config().asMap()).build();
RecordAdapter.register(anotherHolder);
anotherHolder.initialize(ConfigB.class);
System.out.println("Another Device ID: " + ConfigB.VAL.resolve().id());
System.out.println("Another Device Name: " + ConfigB.VAL.resolve().name());
System.out.println("Another Device Serial: " + ConfigB.VAL.resolve().serial());
System.out.println("Another Chip ID: " + ConfigB.VAL.resolve().chip().id());
System.out.println("Another Chip Serial: " + ConfigB.VAL.resolve().chip().serialNumber());
System.out.println("users: " + ConfigB.VAL.resolve().users().size());
for (User user : ConfigB.VAL.resolve().users()) {
System.out.println("Another Users: " + user.name() + ", Age: " + user.age());
}
public record Device(String id, String name, UUID serial, Chip chip) {
}
public record Chip(String id, String serialNumber) {
record User(String name, int age) {
}
record Device(String id, String name, UUID serial, Chip chip, List<User> users) {
}
record Chip(String id, String serialNumber) {
}
static void printMap(Map<String, Object> map, int indent) {
String indentStr = " ".repeat(indent);
for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -62,6 +99,7 @@ public class RecordTest {
System.out.println(indentStr + entry.getKey() + ":");
for (Object item : subList) {
if (item instanceof Map<?, ?> itemMap) {
System.out.println(indentStr + " - ");
printMap((Map<String, Object>) itemMap, indent + 2);
} else {
System.out.println(indentStr + " - " + item);
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -16,7 +16,7 @@
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<packaging>pom</packaging>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<modules>
<module>core</module>
<module>features/section</module>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>cc.carm.lib</groupId>
<artifactId>configured-parent</artifactId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
@@ -4,6 +4,11 @@ import cc.carm.lib.configuration.source.ConfigurationFactory;
import cc.carm.lib.configuration.source.ConfigurationHolder;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class TempConfigFactory
extends ConfigurationFactory<TempSource, ConfigurationHolder<TempSource>, TempConfigFactory> {
@@ -11,6 +16,25 @@ public class TempConfigFactory
return new TempConfigFactory();
}
protected Map<String, Object> defaults = new LinkedHashMap<>();
public TempConfigFactory defaults(@NotNull Map<String, Object> defaults) {
this.defaults = defaults;
return this;
}
public TempConfigFactory defaults(Supplier<Map<String, Object>> defaultsSupplier) {
return defaults(defaultsSupplier.get());
}
public TempConfigFactory defaults(@NotNull Consumer<Map<String, Object>> defaultsConsumer) {
return defaults(() -> {
Map<String, Object> defaults = new LinkedHashMap<>();
defaultsConsumer.accept(defaults);
return defaults;
});
}
@Override
protected TempConfigFactory self() {
return this;
@@ -20,7 +44,7 @@ public class TempConfigFactory
public @NotNull ConfigurationHolder<TempSource> build() {
return new ConfigurationHolder<TempSource>(this.adapters, this.options, this.metadata, this.initializer) {
final @NotNull TempSource source = new TempSource(this);
final @NotNull TempSource source = new TempSource(this, defaults);
@Override
public @NotNull TempSource config() {
@@ -4,18 +4,19 @@ import cc.carm.lib.configuration.source.ConfigurationHolder;
import cc.carm.lib.configuration.source.section.ConfigureSource;
import cc.carm.lib.configuration.source.section.SourcedSection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Objects;
public class TempSource extends ConfigureSource<SourcedSection, Map<String, Object>, TempSource> {
protected @Nullable SourcedSection rootSection;
protected TempSource(@NotNull ConfigurationHolder<? extends TempSource> holder) {
protected @NotNull SourcedSection rootSection;
protected TempSource(@NotNull ConfigurationHolder<? extends TempSource> holder,
@NotNull Map<String, Object> defaults) {
super(holder, 0);
this.rootSection = SourcedSection.root(this);
this.rootSection = SourcedSection.root(this, defaults);
}
@Override
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>configured-parent</artifactId>
<groupId>cc.carm.lib</groupId>
<version>4.1.6</version>
<version>4.1.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>