diff --git a/.gitignore b/.gitignore index c6d98d1..5ff6309 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,38 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a0e1fc4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..27ee5de --- /dev/null +++ b/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + com.imyeyu.config + timi-config + 0.0.1 + + + true + 21 + 21 + UTF-8 + + + + + org.yaml + snakeyaml + 2.2 + + + com.imyeyu.io + timi-io + 0.0.1 + + + org.junit.jupiter + junit-jupiter + 5.10.3 + test + + + org.projectlombok + lombok + 1.18.34 + test + + + diff --git a/src/main/java/com/imyeyu/config/BaseConverter.java b/src/main/java/com/imyeyu/config/BaseConverter.java new file mode 100644 index 0000000..80a4f9e --- /dev/null +++ b/src/main/java/com/imyeyu/config/BaseConverter.java @@ -0,0 +1,36 @@ +package com.imyeyu.config; + +import java.lang.reflect.Field; + +/** + * @author 夜雨 + * @version 2024-04-12 00:44 + */ +public abstract class BaseConverter { + + @SuppressWarnings("unchecked") + K serialize0(Field field, Object t) { + return serialize(field, (T) t); + } + + @SuppressWarnings("unchecked") + T deserialize0(Field field, Object data) { + return deserialize(field, (K) data); + } + + /** + * 当组件值需要写入配置时的转换,默认直接 toString + * + * @param t 组件值类型 + * @return 配置值 + */ + protected abstract K serialize(Field field, T t); + + /** + * 当获取配置并即将设置到组件时调用 + * + * @param data 配置值 + * @return 返回值 + */ + protected abstract T deserialize(Field field, K data); +} diff --git a/src/main/java/com/imyeyu/config/ConfigLoader.java b/src/main/java/com/imyeyu/config/ConfigLoader.java new file mode 100644 index 0000000..5502245 --- /dev/null +++ b/src/main/java/com/imyeyu/config/ConfigLoader.java @@ -0,0 +1,237 @@ +package com.imyeyu.config; + +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.java.ref.RefField; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @author 夜雨 + * @version 2024-04-10 00:44 + */ +public class ConfigLoader { + + private final File file; + private final Yaml yaml; + private final Class clazz; + private final List rawMappingList = new ArrayList<>(); + private final Map, BaseConverter> converterMap = new HashMap<>(); + + private T value; + private LinkedHashMap rawValue; + + public ConfigLoader(String srcPath, Class clazz) { + this(srcPath, srcPath, clazz); + } + + public ConfigLoader(String srcPath, String distPath, Class clazz) { + this.clazz = clazz; + + LoaderOptions loaderOptions = new LoaderOptions(); + Constructor constructor = new Constructor(loaderOptions); + + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setIndent(4); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + dumperOptions.setIndicatorIndent(4); + dumperOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN); + dumperOptions.setIndentWithIndicator(true); + Representer representer = new ConfigRepresenter(dumperOptions); + + yaml = new Yaml(constructor, representer, dumperOptions); + + file = new File(distPath); + + if (!file.exists()) { + try { + IO.resourceToDisk(getClass(), srcPath, distPath); + } catch (Exception e) { + throw new TimiException(TimiCode.ERROR, "load default config error", e); + } + } + } + + @SuppressWarnings("unchecked") + public T load() { + try { + rawValue = yaml.load(IO.toString(file).replaceAll("\t", " ")); + + if (clazz == null || clazz.equals(Object.class)) { + value = (T) rawValue; + } else { + // 解析配置对象 + rawMappingList.clear(); + value = clazz.getDeclaredConstructor().newInstance(); + deserializeToValue(value, rawValue, ""); + } + return this.value; + } catch (Exception e) { + throw new TimiException(TimiCode.ERROR, "load config error", e); + } + } + + public void dump() { + try { + serializeToRawValue(); + IO.toFile(file, yaml.dump(rawValue).replaceAll("( ){4}", "\t")); + } catch (Exception e) { + throw new TimiException(TimiCode.ERROR, "dump config error", e); + } + } + + public void addConverter(Class type, BaseConverter converter) { + converterMap.put(type, converter); + } + + public void removeConverter(Class type) { + converterMap.remove(type); + } + + @SuppressWarnings("unchecked") + private void serializeToRawValue() throws Exception { + for (int i = 0; i < rawMappingList.size(); i++) { + RawMapper rawMapper = rawMappingList.get(i); + String[] bukkit = rawMapper.rawStack.split("#"); + + Map raw = rawValue; + String key; + if (bukkit.length == 1) { + key = bukkit[0]; + } else { + for (int j = 0; j < bukkit.length - 1; j++) { + raw = (Map) rawValue.get(bukkit[j]); + } + key = bukkit[bukkit.length - 1]; + } + Object value = rawMapper.refField.getGetter().invoke(rawMapper.owner); + + // 字段注解序列化 + Converter fieldConverter = rawMapper.refField.getType().getAnnotation(Converter.class); + if (fieldConverter != null) { + BaseConverter converter = Ref.newInstance(fieldConverter.value()); + raw.put(key, converter.serialize0(rawMapper.refField.getField(), value)); + continue; + } + // 类型序列化 + if (converterMap.containsKey(rawMapper.refField.getType())) { + BaseConverter converter = converterMap.get(rawMapper.refField.getType()); + raw.put(key, converter.serialize0(rawMapper.refField.getField(), value)); + continue; + } + // 枚举序列化 + if (Enum.class.isAssignableFrom(rawMapper.refField.getType())) { + raw.put(key, rawMapper.refField.getGetter().invoke(rawMapper.owner).toString()); + continue; + } + // 默认序列化 + raw.put(key, rawMapper.refField.getGetter().invoke(rawMapper.owner)); + } + } + + @SuppressWarnings("unchecked") + private void deserializeToValue(Object object, Map map, String stack) throws Exception { + for (Map.Entry item : map.entrySet()) { + RefField refField = Ref.field(object.getClass(), item.getKey().toString()); + + if (!(item.getValue() instanceof Map)) { + // 缓存映射 + RawMapper rawMapper = new RawMapper(); + rawMapper.owner = object; + rawMapper.refField = refField; + rawMapper.rawStack = stack + item.getKey(); + rawMappingList.add(rawMapper); + } + + Class type = refField.getType(); + // 字段注解反序列化 + Converter fieldConverter = refField.getType().getAnnotation(Converter.class); + if (fieldConverter != null) { + BaseConverter converter = Ref.newInstance(fieldConverter.value()); + refField.getSetter().invoke(object, converter.deserialize0(refField.getField(), item.getValue())); + continue; + } + // 类型反序列化 + if (converterMap.containsKey(type)) { + refField.getSetter().invoke(object, converterMap.get(type).deserialize0(refField.getField(), item.getValue())); + continue; + } + // 默认反序列化 + if (item.getValue() instanceof Map valueMap) { + // 子级对象 + Object newObject = type.getDeclaredConstructor().newInstance(); + refField.getSetter().invoke(object, newObject); + deserializeToValue(newObject, valueMap, stack + item.getKey() + "#"); + continue; + } + // 数组 + if (item.getValue() instanceof List valueList) { + // 数组 + if (type.isArray()) { + Class arrayType = type.getComponentType(); + Object array = Array.newInstance(arrayType, valueList.size()); + if (arrayType.isPrimitive()) { + // 基本类型数组 + if (TimiJava.isNotEmpty(valueList)) { + Method method = Ref.getMethod(valueList.get(0).getClass(), arrayType.getName() + "Value"); + for (int i = 0; i < valueList.size(); i++) { + Array.set(array, i, method.invoke(valueList.get(i))); + } + } + } else { + for (int i = 0; i < valueList.size(); i++) { + Array.set(array, i, arrayType.cast(valueList.get(i))); + } + } + refField.getSetter().invoke(object, array); + continue; + } + // 列表 + refField.getSetter().invoke(object, valueList); + continue; + } + // 枚举 + if (Enum.class.isAssignableFrom(type)) { + refField.getSetter().invoke(object, Ref.toType((Class>) type, item.getValue().toString())); + continue; + } + refField.getSetter().invoke(object, item.getValue()); + } + } + + public T getValue() { + return value; + } + + public Map getRawValue() { + return rawValue; + } + + /** + * @author 夜雨 + * @version 2024-04-10 00:45 + */ + private static class RawMapper { + + Object owner; + + RefField refField; + + String rawStack; + } +} diff --git a/src/main/java/com/imyeyu/config/ConfigRepresenter.java b/src/main/java/com/imyeyu/config/ConfigRepresenter.java new file mode 100644 index 0000000..acd5822 --- /dev/null +++ b/src/main/java/com/imyeyu/config/ConfigRepresenter.java @@ -0,0 +1,18 @@ +package com.imyeyu.config; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +/** + * @author 夜雨 + * @version 2024-05-01 14:03 + */ +public class ConfigRepresenter extends Representer { + + public ConfigRepresenter(DumperOptions options) { + super(options); + + this.nullRepresenter = data -> representScalar(Tag.NULL, ""); + } +} diff --git a/src/main/java/com/imyeyu/config/Converter.java b/src/main/java/com/imyeyu/config/Converter.java new file mode 100644 index 0000000..8b801f5 --- /dev/null +++ b/src/main/java/com/imyeyu/config/Converter.java @@ -0,0 +1,14 @@ +package com.imyeyu.config; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author 夜雨 + * @version 2024-04-26 00:30 + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Converter { + + Class> value(); +} diff --git a/src/main/java/com/imyeyu/config/StringConverter.java b/src/main/java/com/imyeyu/config/StringConverter.java new file mode 100644 index 0000000..89c8010 --- /dev/null +++ b/src/main/java/com/imyeyu/config/StringConverter.java @@ -0,0 +1,15 @@ +package com.imyeyu.config; + +import java.lang.reflect.Field; + +/** + * @author 夜雨 + * @version 2024-04-27 10:38 + */ +public abstract class StringConverter extends BaseConverter { + + @Override + protected String serialize(Field field, T t) { + return t.toString(); + } +} diff --git a/src/test/java/test/Config.java b/src/test/java/test/Config.java new file mode 100644 index 0000000..be806cd --- /dev/null +++ b/src/test/java/test/Config.java @@ -0,0 +1,42 @@ +package test; + +import lombok.Data; + +import java.util.List; + +/** + * @author 夜雨 + * @version 2024-04-25 15:37 + */ +@Data +public class Config { + + public enum EnumTest { + + TEST1, + + TEST2 + } + + private String str; + + private int num; + + private Obj obj; + + private boolean bool; + + private EnumTest enumTest; + + private ConfigConverterTest converterTest; + + private int[] list; + + @Data + public static class Obj { + + private String objStr; + + private List objList; + } +} \ No newline at end of file diff --git a/src/test/java/test/ConfigConverterTest.java b/src/test/java/test/ConfigConverterTest.java new file mode 100644 index 0000000..1bf3cac --- /dev/null +++ b/src/test/java/test/ConfigConverterTest.java @@ -0,0 +1,13 @@ +package test; + +import lombok.Data; + +/** + * @author 夜雨 + * @version 2024-04-26 18:08 + */ +@Data +public class ConfigConverterTest { + + private String value; +} diff --git a/src/test/java/test/TestConfig.java b/src/test/java/test/TestConfig.java new file mode 100644 index 0000000..0f468db --- /dev/null +++ b/src/test/java/test/TestConfig.java @@ -0,0 +1,106 @@ +package test; + +import com.imyeyu.config.BaseConverter; +import com.imyeyu.config.ConfigLoader; +import com.imyeyu.io.IO; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.Map; + +/** + * @author 夜雨 + * @version 2024-04-25 11:13 + */ +public class TestConfig { + + private static final String FILE_PATH = "test.yaml"; + + public ConfigLoader load(Class clazz) { + ConfigLoader loader = new ConfigLoader<>(FILE_PATH, clazz); + loader.load(); + return loader; + } + + public ConfigLoader reload(Class clazz) { + IO.destroy(new File(FILE_PATH)); + return load(clazz); + } + + @Test + public void testLoad() { + ConfigLoader loader = reload(null); + loader.load(); + assert loader.getRawValue() != null; + } + + @Test + public void testRaw() { + ConfigLoader loader = reload(Object.class); + Map rawValue = loader.getRawValue(); + rawValue.put("str", "str value update"); + loader.dump(); + loader.load(); + rawValue = loader.getRawValue(); + assert rawValue.get("str").equals("str value update"); + } + + @Test + public void testObject() { + ConfigLoader loader = reload(Config.class); + Config value = loader.getValue(); + assert value.getStr().equals("str value"); + assert value.getNum() == 123; + assert value.getList().length == 4; + assert value.getObj() != null; + assert value.getObj().getObjStr().equals("test str"); + assert value.getObj().getObjList().size() == 2; + + value.setStr("dump object test"); + value.getObj().getObjList().add(0, "new list item test"); + loader.dump(); + + value = loader.load(); + assert value.getStr().equals("dump object test"); + assert value.getObj().getObjList().get(0).equals("new list item test"); + } + + @Test + public void testEnum() { + ConfigLoader loader = reload(Config.class); + Config value = loader.getValue(); + assert value.getEnumTest() == Config.EnumTest.TEST2; + value.setEnumTest(Config.EnumTest.TEST1); + loader.dump(); + value = loader.load(); + assert value.getEnumTest() == Config.EnumTest.TEST1; + } + + @Test + public void testConverter() { + IO.destroy(new File("test_converter.yaml")); + + ConfigLoader loader = new ConfigLoader<>("test_converter.yaml", Config.class); + loader.addConverter(ConfigConverterTest.class, new BaseConverter() { + + @Override + protected String serialize(Field field, ConfigConverterTest configConverterTest) { + return configConverterTest.getValue(); + } + + @Override + protected ConfigConverterTest deserialize(Field field, String data) { + ConfigConverterTest configConverterTest = new ConfigConverterTest(); + configConverterTest.setValue(data); + return configConverterTest; + } + }); + Config value = loader.load(); + assert value.getConverterTest().getValue().equals("converter value"); + value.getConverterTest().setValue("converter test update"); + loader.dump(); + value = loader.load(); + assert value.getConverterTest().getValue().equals("converter test update"); + } +} diff --git a/src/test/resources/test.yaml b/src/test/resources/test.yaml new file mode 100644 index 0000000..5491599 --- /dev/null +++ b/src/test/resources/test.yaml @@ -0,0 +1,16 @@ +# comment +str: "str value" +num: 123 +bool: true +enum_test: TEST2 +obj: + # comment 2 + obj_str: test str + obj_list: + - str item0 + - str item1 +list: + - 0 + - 1 + - 2 + - 3 diff --git a/src/test/resources/test_converter.yaml b/src/test/resources/test_converter.yaml new file mode 100644 index 0000000..54cbdef --- /dev/null +++ b/src/test/resources/test_converter.yaml @@ -0,0 +1,17 @@ +# comment +str: "str value" +num: 123 +bool: true +enum_test: TEST2 +converter_test: converter value +obj: + # comment 2 + obj_str: test str + obj_list: + - str item0 + - str item1 +list: + - 0 + - 1 + - 2 + - 3 diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000..f2cfd18 --- /dev/null +++ b/test.yaml @@ -0,0 +1,15 @@ +str: dump object test +num: 123 +bool: true +enum_test: TEST2 +obj: + obj_str: test str + obj_list: + - new list item test + - str item0 + - str item1 +list: + - 0 + - 1 + - 2 + - 3 diff --git a/test_converter.yaml b/test_converter.yaml new file mode 100644 index 0000000..5b8b363 --- /dev/null +++ b/test_converter.yaml @@ -0,0 +1,15 @@ +str: str value +num: 123 +bool: true +enum_test: TEST2 +converter_test: converter test update +obj: + obj_str: test str + obj_list: + - str item0 + - str item1 +list: + - 0 + - 1 + - 2 + - 3