From 8c1214612af3499afd331daf868462f3e33b2e02 Mon Sep 17 00:00:00 2001 From: carm Date: Wed, 25 Jun 2025 09:31:07 +0800 Subject: [PATCH] feat(adapter): Support complex ParameterizedType's serialize & deserialize. --- core/pom.xml | 2 +- .../adapter/ValueAdapterRegistry.java | 105 ++++++++++++++++-- demo/pom.xml | 2 +- .../configuration/tests/test/YamlTests.java | 2 +- features/commentable/pom.xml | 2 +- features/file/pom.xml | 2 +- features/kotlin/pom.xml | 2 +- features/record/pom.xml | 2 +- .../adapter/record/RecordAdapter.java | 20 ++-- features/record/src/test/java/RecordTest.java | 72 +++++++++--- features/section/pom.xml | 2 +- features/text/pom.xml | 2 +- features/validators/pom.xml | 2 +- features/versioned/pom.xml | 2 +- pom.xml | 2 +- providers/gson/pom.xml | 2 +- providers/hocon/pom.xml | 2 +- providers/mongodb/pom.xml | 2 +- providers/sql/pom.xml | 2 +- providers/temp/pom.xml | 2 +- .../source/temp/TempConfigFactory.java | 28 ++++- .../configuration/source/temp/TempSource.java | 9 +- providers/yaml/pom.xml | 2 +- 23 files changed, 213 insertions(+), 57 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 75f007e..107ff1a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT 4.0.0 diff --git a/core/src/main/java/cc/carm/lib/configuration/adapter/ValueAdapterRegistry.java b/core/src/main/java/cc/carm/lib/configuration/adapter/ValueAdapterRegistry.java index 2a833c3..674fa90 100644 --- a/core/src/main/java/cc/carm/lib/configuration/adapter/ValueAdapterRegistry.java +++ b/core/src/main/java/cc/carm/lib/configuration/adapter/ValueAdapterRegistry.java @@ -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 deserialize(@NotNull ConfigurationHolder holder, @NotNull ValueType 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 adapter = adapterOf(type); - if (adapter == null) throw new RuntimeException("No adapter for type " + type); - return adapter.parse(holder, type, source); + if (!(type.getType() instanceof ParameterizedType) && type.isInstance(source)) { + return type.cast(source); // Not required to deserialize + } + + ValueAdapter 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 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 Object serialize(@NotNull ConfigurationHolder holder, @Nullable T value) throws Exception { if (value == null) return null; // Null check + ValueType type = ValueType.of(value); ValueAdapter 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 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 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 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 List deserializeList(@NotNull ConfigurationHolder holder, + @NotNull ValueType type, @Nullable Object source) throws Exception { + if (source == null) return Collections.emptyList(); // Null check + if (source instanceof List) { + List list = (List) source; + List 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(); + } + } } diff --git a/demo/pom.xml b/demo/pom.xml index ad2867d..1cd93eb 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -5,7 +5,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT 4.0.0 diff --git a/demo/src/test/java/cc/carm/lib/configuration/tests/test/YamlTests.java b/demo/src/test/java/cc/carm/lib/configuration/tests/test/YamlTests.java index fef6724..5770fa9 100644 --- a/demo/src/test/java/cc/carm/lib/configuration/tests/test/YamlTests.java +++ b/demo/src/test/java/cc/carm/lib/configuration/tests/test/YamlTests.java @@ -14,7 +14,7 @@ import java.util.Map; public class YamlTests { @Test - public void test() { + public void test() throws Exception { ConfigurationHolder holder = YAMLConfigFactory.from("target/tests.yml") .resourcePath("configs/sample.yml").build(); diff --git a/features/commentable/pom.xml b/features/commentable/pom.xml index dc36123..4fb22ae 100644 --- a/features/commentable/pom.xml +++ b/features/commentable/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/file/pom.xml b/features/file/pom.xml index 5e0c270..ba270b5 100644 --- a/features/file/pom.xml +++ b/features/file/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/kotlin/pom.xml b/features/kotlin/pom.xml index c971907..8d05ecb 100644 --- a/features/kotlin/pom.xml +++ b/features/kotlin/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/record/pom.xml b/features/record/pom.xml index 4374b9c..4683bf1 100644 --- a/features/record/pom.xml +++ b/features/record/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/record/src/main/java/cc/carm/lib/configured/adapter/record/RecordAdapter.java b/features/record/src/main/java/cc/carm/lib/configured/adapter/record/RecordAdapter.java index dabc43e..c585e62 100644 --- a/features/record/src/main/java/cc/carm/lib/configured/adapter/record/RecordAdapter.java +++ b/features/record/src/main/java/cc/carm/lib/configured/adapter/record/RecordAdapter.java @@ -38,10 +38,14 @@ public class RecordAdapter extends ValueAdapter { return (holder, type1, r) -> toMap(holder, r); } + @SuppressWarnings("unchecked") public static ValueParser parser(@NotNull ValueType 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) valueType.getRawType(), section.asMap()); + } else if (value instanceof Map map) { + return fromMap(holder, (Class) valueType.getRawType(), (Map) map); + } else return null; }; } @@ -57,6 +61,7 @@ public class RecordAdapter extends ValueAdapter { 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 extends ValueAdapter { 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) value); + if (component.getType().isRecord()) { + return fromMap(holder, component.getType().asSubclass(Record.class), (Map) 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 { diff --git a/features/record/src/test/java/RecordTest.java b/features/record/src/test/java/RecordTest.java index 421c5ef..f7171c6 100644 --- a/features/record/src/test/java/RecordTest.java +++ b/features/record/src/test/java/RecordTest.java @@ -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 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 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); + +// try { +// List 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()); } - printMap(holder.config().asMap(), 0); } - - public record Device(String id, String name, UUID serial, Chip chip) { + record User(String name, int age) { } - public record Chip(String id, String serialNumber) { + record Device(String id, String name, UUID serial, Chip chip, List users) { } + record Chip(String id, String serialNumber) { + } + + static void printMap(Map map, int indent) { String indentStr = " ".repeat(indent); for (Map.Entry 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) itemMap, indent + 2); } else { System.out.println(indentStr + " - " + item); diff --git a/features/section/pom.xml b/features/section/pom.xml index 09442d5..ff4159f 100644 --- a/features/section/pom.xml +++ b/features/section/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/text/pom.xml b/features/text/pom.xml index be48dca..270ddb1 100644 --- a/features/text/pom.xml +++ b/features/text/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/validators/pom.xml b/features/validators/pom.xml index 7e7e423..871b541 100644 --- a/features/validators/pom.xml +++ b/features/validators/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/features/versioned/pom.xml b/features/versioned/pom.xml index 878c8b6..2ecd449 100644 --- a/features/versioned/pom.xml +++ b/features/versioned/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/pom.xml b/pom.xml index a3b30a2..e0caf44 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ cc.carm.lib configured-parent pom - 4.1.6 + 4.1.7-SNAPSHOT core features/section diff --git a/providers/gson/pom.xml b/providers/gson/pom.xml index adfd6c2..9525df3 100644 --- a/providers/gson/pom.xml +++ b/providers/gson/pom.xml @@ -5,7 +5,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/providers/hocon/pom.xml b/providers/hocon/pom.xml index 7b068db..ab599d5 100644 --- a/providers/hocon/pom.xml +++ b/providers/hocon/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib configured-parent - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/providers/mongodb/pom.xml b/providers/mongodb/pom.xml index 418cb9b..fb49b15 100644 --- a/providers/mongodb/pom.xml +++ b/providers/mongodb/pom.xml @@ -5,7 +5,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/providers/sql/pom.xml b/providers/sql/pom.xml index a34fa5a..2240096 100644 --- a/providers/sql/pom.xml +++ b/providers/sql/pom.xml @@ -6,7 +6,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/providers/temp/pom.xml b/providers/temp/pom.xml index 8a95d26..6af628c 100644 --- a/providers/temp/pom.xml +++ b/providers/temp/pom.xml @@ -6,7 +6,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml diff --git a/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempConfigFactory.java b/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempConfigFactory.java index a4492d7..9eecd61 100644 --- a/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempConfigFactory.java +++ b/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempConfigFactory.java @@ -4,13 +4,37 @@ 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, TempConfigFactory> { + extends ConfigurationFactory, TempConfigFactory> { public static @NotNull TempConfigFactory create() { return new TempConfigFactory(); } + protected Map defaults = new LinkedHashMap<>(); + + public TempConfigFactory defaults(@NotNull Map defaults) { + this.defaults = defaults; + return this; + } + + public TempConfigFactory defaults(Supplier> defaultsSupplier) { + return defaults(defaultsSupplier.get()); + } + + public TempConfigFactory defaults(@NotNull Consumer> defaultsConsumer) { + return defaults(() -> { + Map defaults = new LinkedHashMap<>(); + defaultsConsumer.accept(defaults); + return defaults; + }); + } + @Override protected TempConfigFactory self() { return this; @@ -20,7 +44,7 @@ public class TempConfigFactory public @NotNull ConfigurationHolder build() { return new ConfigurationHolder(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() { diff --git a/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempSource.java b/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempSource.java index 8d25bf1..ae3de64 100644 --- a/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempSource.java +++ b/providers/temp/src/main/java/cc/carm/lib/configuration/source/temp/TempSource.java @@ -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, TempSource> { - protected @Nullable SourcedSection rootSection; - protected TempSource(@NotNull ConfigurationHolder holder) { + protected @NotNull SourcedSection rootSection; + + protected TempSource(@NotNull ConfigurationHolder holder, + @NotNull Map defaults) { super(holder, 0); - this.rootSection = SourcedSection.root(this); + this.rootSection = SourcedSection.root(this, defaults); } @Override diff --git a/providers/yaml/pom.xml b/providers/yaml/pom.xml index 9d0002d..1d2341f 100644 --- a/providers/yaml/pom.xml +++ b/providers/yaml/pom.xml @@ -6,7 +6,7 @@ configured-parent cc.carm.lib - 4.1.6 + 4.1.7-SNAPSHOT ../../pom.xml