From 844cbfab53e57783d84aa03defa5f0fdeeb9cbe9 Mon Sep 17 00:00:00 2001 From: carm Date: Thu, 27 Feb 2025 13:32:12 +0800 Subject: [PATCH] feat(sql): Try to implement sql source --- .../lib/configuration/adapter/ValueType.java | 5 +- .../source/ConfigurationHolder.java | 20 +- .../loader/ConfigInitializeHandler.java | 25 +-- .../loader/ConfigurationInitializer.java | 48 ++--- .../source/meta/StandardMeta.java | 6 +- .../configuration/value/ValueManifest.java | 7 +- .../demo/DatabaseConfiguration.java | 18 +- .../source/section/AbstractMapSection.java | 10 + .../annotation/ConfigVersion.java | 3 +- .../option/VersionedOptions.java | 13 -- .../VersionedMetaTypes.java | 15 +- pom.xml | 2 +- providers/sql/README.md | 20 +- providers/sql/pom.xml | 25 ++- .../source/sql/SQLConfigFactory.java | 146 ++++++++++++++ .../configuration/source/sql/SQLSource.java | 179 ++++++++++++------ .../source/sql/SQLValueResolver.java | 103 ++++++++++ .../src/test/java/config/SQLConfigTest.java | 40 ++-- 18 files changed, 526 insertions(+), 159 deletions(-) delete mode 100644 features/versioned/src/main/java/cc/carm/lib/configuration/option/VersionedOptions.java rename features/versioned/src/main/java/cc/carm/lib/configuration/{commentable => versioned}/VersionedMetaTypes.java (53%) create mode 100644 providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLConfigFactory.java create mode 100644 providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLValueResolver.java diff --git a/core/src/main/java/cc/carm/lib/configuration/adapter/ValueType.java b/core/src/main/java/cc/carm/lib/configuration/adapter/ValueType.java index 23a36ab..89a7b4c 100644 --- a/core/src/main/java/cc/carm/lib/configuration/adapter/ValueType.java +++ b/core/src/main/java/cc/carm/lib/configuration/adapter/ValueType.java @@ -4,7 +4,6 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.Map; import java.util.Objects; /** @@ -30,6 +29,7 @@ public abstract class ValueType { public static final ValueType CHAR = ofPrimitiveType(Character.class); public static final ValueType CHAR_TYPE = ofPrimitiveType(char.class); + public static final ValueType[] PRIMITIVE_TYPES = { STRING, INTEGER, LONG, DOUBLE, FLOAT, BOOLEAN, BYTE, SHORT, CHAR, INTEGER_TYPE, LONG_TYPE, DOUBLE_TYPE, FLOAT_TYPE, BOOLEAN_TYPE, BYTE_TYPE, SHORT_TYPE, CHAR_TYPE @@ -157,7 +157,7 @@ public abstract class ValueType { ParameterizedType pt = (ParameterizedType) type; Type raw = pt.getRawType(); StringBuilder sb = new StringBuilder(); - sb.append(raw.getTypeName()); + sb.append(raw.getClass().getName()); sb.append('<'); Type[] args = pt.getActualTypeArguments(); for (int i = 0; i < args.length; i++) { @@ -166,6 +166,7 @@ public abstract class ValueType { } sb.append(args[i].getTypeName()); } + sb.append('>'); return sb.toString(); } return type.getTypeName(); diff --git a/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationHolder.java b/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationHolder.java index 3b73614..2aa4f56 100644 --- a/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationHolder.java +++ b/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationHolder.java @@ -6,9 +6,11 @@ import cc.carm.lib.configuration.adapter.ValueType; import cc.carm.lib.configuration.source.loader.ConfigurationInitializer; import cc.carm.lib.configuration.source.meta.ConfigurationMetaHolder; import cc.carm.lib.configuration.source.meta.ConfigurationMetadata; +import cc.carm.lib.configuration.source.meta.StandardMeta; import cc.carm.lib.configuration.source.option.ConfigurationOption; import cc.carm.lib.configuration.source.option.ConfigurationOptionHolder; import cc.carm.lib.configuration.source.section.ConfigureSource; +import cc.carm.lib.configuration.value.ConfigValue; import cc.carm.lib.configuration.value.ValueManifest; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -18,6 +20,8 @@ import org.jetbrains.annotations.UnmodifiableView; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; public abstract class ConfigurationHolder> { @@ -51,7 +55,7 @@ public abstract class ConfigurationHolder O option(@NotNull ConfigurationOption option) { + public @NotNull O option(@NotNull ConfigurationOption option) { return options().get(option); } @@ -66,14 +70,26 @@ public abstract class ConfigurationHolder Map extractMetadata(@NotNull ConfigurationMetadata type) { + return extractMetadata(type, Objects::nonNull); + } + + @NotNull + @UnmodifiableView + public Map extractMetadata(@NotNull ConfigurationMetadata type, @NotNull Predicate filter) { Map metas = new LinkedHashMap<>(); for (Map.Entry entry : this.metadata.entrySet()) { M data = entry.getValue().get(type); - if (data != null) metas.put(entry.getKey(), data); + if (filter.test(data)) metas.put(entry.getKey(), data); } return Collections.unmodifiableMap(metas); } + @NotNull + @UnmodifiableView + public Map> registeredValues() { + return extractMetadata(StandardMeta.VALUE); + } + public ValueAdapterRegistry adapters() { return this.adapters; } diff --git a/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigInitializeHandler.java b/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigInitializeHandler.java index 1b1ba35..6823bc7 100644 --- a/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigInitializeHandler.java +++ b/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigInitializeHandler.java @@ -5,26 +5,27 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @FunctionalInterface -public interface ConfigInitializeHandler { +public interface ConfigInitializeHandler { - static ConfigInitializeHandler start() { - return (provider, path, value) -> { + static ConfigInitializeHandler start() { + return (provider, path, value, instace) -> { }; } - void whenInitialize(@NotNull ConfigurationHolder holder, @Nullable String path, @NotNull T value) throws Exception; + void whenInitialize(@NotNull ConfigurationHolder holder, @Nullable String path, + @NotNull T value, @Nullable V instance) throws Exception; - default ConfigInitializeHandler andThen(ConfigInitializeHandler after) { - return (provider, path, value) -> { - whenInitialize(provider, path, value); - after.whenInitialize(provider, path, value); + default ConfigInitializeHandler andThen(ConfigInitializeHandler after) { + return (provider, path, value, instance) -> { + whenInitialize(provider, path, value, instance); + after.whenInitialize(provider, path, value, instance); }; } - default ConfigInitializeHandler compose(ConfigInitializeHandler before) { - return (provider, path, value) -> { - before.whenInitialize(provider, path, value); - whenInitialize(provider, path, value); + default ConfigInitializeHandler compose(ConfigInitializeHandler before) { + return (provider, path, value, instance) -> { + before.whenInitialize(provider, path, value, instance); + whenInitialize(provider, path, value, instance); }; } diff --git a/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigurationInitializer.java b/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigurationInitializer.java index 4f69dc3..69def1b 100644 --- a/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigurationInitializer.java +++ b/core/src/main/java/cc/carm/lib/configuration/source/loader/ConfigurationInitializer.java @@ -21,18 +21,18 @@ import java.util.function.Function; public class ConfigurationInitializer { protected @NotNull PathGenerator pathGenerator; - protected @NotNull ConfigInitializeHandler fieldInitializer; - protected @NotNull ConfigInitializeHandler> classInitializer; + protected @NotNull ConfigInitializeHandler> valueInitializer; + protected @NotNull ConfigInitializeHandler, Object> classInitializer; public ConfigurationInitializer() { this(PathGenerator.of(), ConfigInitializeHandler.start(), ConfigInitializeHandler.start()); } public ConfigurationInitializer(@NotNull PathGenerator pathGenerator, - @NotNull ConfigInitializeHandler fieldInitializer, - @NotNull ConfigInitializeHandler> classInitializer) { + @NotNull ConfigInitializeHandler> valueInitializer, + @NotNull ConfigInitializeHandler, Object> classInitializer) { this.pathGenerator = pathGenerator; - this.fieldInitializer = fieldInitializer; + this.valueInitializer = valueInitializer; this.classInitializer = classInitializer; } @@ -44,34 +44,34 @@ public class ConfigurationInitializer { return pathGenerator; } - public ConfigInitializeHandler fieldInitializer() { - return fieldInitializer; + public ConfigInitializeHandler> fieldInitializer() { + return valueInitializer; } - public void fieldInitializer(@NotNull ConfigInitializeHandler fieldInitializer) { - this.fieldInitializer = fieldInitializer; + public void fieldInitializer(@NotNull ConfigInitializeHandler> fieldInitializer) { + this.valueInitializer = fieldInitializer; } - public ConfigInitializeHandler> classInitializer() { + public ConfigInitializeHandler, Object> classInitializer() { return classInitializer; } - public void classInitializer(@NotNull ConfigInitializeHandler> classInitializer) { + public void classInitializer(@NotNull ConfigInitializeHandler, Object> classInitializer) { this.classInitializer = classInitializer; } - public void appendFieldInitializer(@NotNull ConfigInitializeHandler fieldInitializer) { - this.fieldInitializer = this.fieldInitializer.andThen(fieldInitializer); + public void appendFieldInitializer(@NotNull ConfigInitializeHandler> fieldInitializer) { + this.valueInitializer = this.valueInitializer.andThen(fieldInitializer); } - public void appendClassInitializer(@NotNull ConfigInitializeHandler> classInitializer) { + public void appendClassInitializer(@NotNull ConfigInitializeHandler, Object> classInitializer) { this.classInitializer = this.classInitializer.andThen(classInitializer); } public void registerClassAnnotation(@NotNull Class annotation, @NotNull ConfigurationMetadata metadata, @NotNull Function extractor) { - appendClassInitializer((holder, path, clazz) -> { + appendClassInitializer((holder, path, clazz, instance) -> { A data = clazz.getAnnotation(annotation); if (data == null) return; holder.metadata(path).setIfAbsent(metadata, extractor.apply(data)); @@ -81,7 +81,7 @@ public class ConfigurationInitializer { public void registerFieldAnnotation(@NotNull Class annotation, @NotNull ConfigurationMetadata metadata, @NotNull Function extractor) { - appendFieldInitializer((holder, path, field) -> { + appendFieldInitializer((holder, path, field, instance) -> { A data = field.getAnnotation(annotation); if (data == null) return; holder.metadata(path).setIfAbsent(metadata, extractor.apply(data)); @@ -123,7 +123,7 @@ public class ConfigurationInitializer { @Nullable String parentPath, @Nullable Field configField) { String path = getClassPath(holder, parentPath, root.getClass(), configField); try { - this.classInitializer.whenInitialize(holder, path, root.getClass()); + this.classInitializer.whenInitialize(holder, path, root.getClass(), root); } catch (Exception e) { e.printStackTrace(); } @@ -140,7 +140,7 @@ public class ConfigurationInitializer { String path = getClassPath(holder, parentPath, clazz, configField); try { - this.classInitializer.whenInitialize(holder, path, (Class) clazz); + this.classInitializer.whenInitialize(holder, path, (Class) clazz, configField); } catch (Exception e) { e.printStackTrace(); } @@ -169,15 +169,15 @@ public class ConfigurationInitializer { String path = getFieldPath(holder, parent, field); if (path == null) return; value.initialize(holder, path); - holder.metadata(path).set(StandardMeta.UNIT, true); // Mark the minimal config value unit. - try { - this.fieldInitializer.whenInitialize(holder, path, field); - } catch (Exception e) { - e.printStackTrace(); - } + holder.metadata(path).set(StandardMeta.VALUE, value); // Mark the minimal config value unit. if (holder.option(StandardOptions.SET_DEFAULTS)) { value.setDefault(); // Set default value. } + try { + this.valueInitializer.whenInitialize(holder, path, field, value); + } catch (Exception e) { + e.printStackTrace(); + } if (holder.option(StandardOptions.PRELOAD)) { value.get(); // Preload the value by calling #get method. } diff --git a/core/src/main/java/cc/carm/lib/configuration/source/meta/StandardMeta.java b/core/src/main/java/cc/carm/lib/configuration/source/meta/StandardMeta.java index 980e1ff..927ee60 100644 --- a/core/src/main/java/cc/carm/lib/configuration/source/meta/StandardMeta.java +++ b/core/src/main/java/cc/carm/lib/configuration/source/meta/StandardMeta.java @@ -1,10 +1,12 @@ package cc.carm.lib.configuration.source.meta; +import cc.carm.lib.configuration.value.ConfigValue; + public interface StandardMeta { /** - * To mark the {@link cc.carm.lib.configuration.value.ConfigValue} as a minimal unit path. + * To mark the {@link ConfigValue} instance of specific path. */ - ConfigurationMetadata UNIT = ConfigurationMetadata.of(false); + ConfigurationMetadata> VALUE = ConfigurationMetadata.of(); } diff --git a/core/src/main/java/cc/carm/lib/configuration/value/ValueManifest.java b/core/src/main/java/cc/carm/lib/configuration/value/ValueManifest.java index ca48ab6..23a7980 100644 --- a/core/src/main/java/cc/carm/lib/configuration/value/ValueManifest.java +++ b/core/src/main/java/cc/carm/lib/configuration/value/ValueManifest.java @@ -20,7 +20,6 @@ public class ValueManifest { protected @NotNull Supplier<@Nullable T> defaultSupplier; - public ValueManifest(@NotNull ValueType type) { this(type, () -> null, EMPTY_INITIALIZER, null, null); } @@ -93,12 +92,12 @@ public class ValueManifest { public @NotNull String path() { if (path != null) return path; - else throw new IllegalStateException("No section path provided."); + else throw new IllegalStateException("No section path provided for Value(" + type() + ")."); } public @NotNull ConfigurationHolder holder() { if (this.holder != null) return this.holder; - throw new IllegalStateException("Value does not have a provider."); + throw new IllegalStateException("Value(" + type() + ") does not have a provider."); } public @NotNull ConfigureSource config() { @@ -118,7 +117,7 @@ public class ValueManifest { } - private static final @NotNull BiConsumer<@NotNull ConfigurationHolder, @NotNull String> EMPTY_INITIALIZER = (provider, path) -> { + private static final @NotNull BiConsumer<@NotNull ConfigurationHolder, @NotNull String> EMPTY_INITIALIZER = (provider, valuePath) -> { }; } diff --git a/demo/src/main/java/cc/carm/lib/configuration/demo/DatabaseConfiguration.java b/demo/src/main/java/cc/carm/lib/configuration/demo/DatabaseConfiguration.java index 02424b3..027e46d 100644 --- a/demo/src/main/java/cc/carm/lib/configuration/demo/DatabaseConfiguration.java +++ b/demo/src/main/java/cc/carm/lib/configuration/demo/DatabaseConfiguration.java @@ -7,7 +7,7 @@ import cc.carm.lib.configuration.value.ConfigValue; import cc.carm.lib.configuration.value.standard.ConfiguredValue; @HeaderComments({"", "数据库配置", " 用于提供数据库连接,进行数据库操作。"}) -public class DatabaseConfiguration implements Configuration { +public interface DatabaseConfiguration extends Configuration { @ConfigPath("driver") @HeaderComments({ @@ -16,18 +16,18 @@ public class DatabaseConfiguration implements Configuration { "- MySQL(新): com.mysql.cj.jdbc.Driver", "- MariaDB(推荐): org.mariadb.jdbc.Driver", }) - protected static final ConfigValue DRIVER_NAME = ConfiguredValue.of( + ConfigValue DRIVER_NAME = ConfiguredValue.of( String.class, "com.mysql.cj.jdbc.Driver" ); - protected static final ConfigValue HOST = ConfiguredValue.of(String.class, "127.0.0.1"); - protected static final ConfigValue PORT = ConfiguredValue.of(Integer.class, 3306); - protected static final ConfigValue DATABASE = ConfiguredValue.of(String.class, "minecraft"); - protected static final ConfigValue USERNAME = ConfiguredValue.of(String.class, "root"); - protected static final ConfigValue PASSWORD = ConfiguredValue.of(String.class, "password"); - protected static final ConfigValue EXTRA = ConfiguredValue.of(String.class, "?useSSL=false"); + ConfigValue HOST = ConfiguredValue.of(String.class, "127.0.0.1"); + ConfigValue PORT = ConfiguredValue.of(Integer.class, 3306); + ConfigValue DATABASE = ConfiguredValue.of(String.class, "minecraft"); + ConfigValue USERNAME = ConfiguredValue.of(String.class, "root"); + ConfigValue PASSWORD = ConfiguredValue.of(String.class, "password"); + ConfigValue EXTRA = ConfiguredValue.of(String.class, "?useSSL=false"); - protected static String buildJDBC() { + static String buildJDBC() { return String.format("jdbc:mysql://%s:%s/%s%s", HOST.get(), PORT.get(), DATABASE.get(), EXTRA.get()); } diff --git a/features/section/src/main/java/cc/carm/lib/configuration/source/section/AbstractMapSection.java b/features/section/src/main/java/cc/carm/lib/configuration/source/section/AbstractMapSection.java index d225248..0bd40f1 100644 --- a/features/section/src/main/java/cc/carm/lib/configuration/source/section/AbstractMapSection.java +++ b/features/section/src/main/java/cc/carm/lib/configuration/source/section/AbstractMapSection.java @@ -81,6 +81,16 @@ public abstract class AbstractMapSection> implem for (Map.Entry entry : this.data.entrySet()) { if (entry.getValue() instanceof AbstractMapSection) { output.put(entry.getKey(), ((AbstractMapSection) entry.getValue()).rawMap()); + } else if (entry.getValue() instanceof List) { + List list = new ArrayList<>(); + for (Object obj : (List) entry.getValue()) { + if (obj instanceof AbstractMapSection) { + list.add(((AbstractMapSection) obj).rawMap()); + } else { + list.add(obj); + } + } + output.put(entry.getKey(), list); } else { output.put(entry.getKey(), entry.getValue()); } diff --git a/features/versioned/src/main/java/cc/carm/lib/configuration/annotation/ConfigVersion.java b/features/versioned/src/main/java/cc/carm/lib/configuration/annotation/ConfigVersion.java index 19ba260..3f31029 100644 --- a/features/versioned/src/main/java/cc/carm/lib/configuration/annotation/ConfigVersion.java +++ b/features/versioned/src/main/java/cc/carm/lib/configuration/annotation/ConfigVersion.java @@ -1,6 +1,5 @@ package cc.carm.lib.configuration.annotation; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Range; import java.lang.annotation.ElementType; @@ -22,7 +21,7 @@ public @interface ConfigVersion { * * @return the version of the configuration field */ - @Range(from = 0, to = Integer.MAX_VALUE) + @Range(from = 0, to = 65535) int value() default 0; } diff --git a/features/versioned/src/main/java/cc/carm/lib/configuration/option/VersionedOptions.java b/features/versioned/src/main/java/cc/carm/lib/configuration/option/VersionedOptions.java deleted file mode 100644 index 4387206..0000000 --- a/features/versioned/src/main/java/cc/carm/lib/configuration/option/VersionedOptions.java +++ /dev/null @@ -1,13 +0,0 @@ -package cc.carm.lib.configuration.option; - -import cc.carm.lib.configuration.source.option.ConfigurationOption; - -public interface VersionedOptions { - - /** - * Whether to set newer defaults when a {@link cc.carm.lib.configuration.value.ConfigValue}'s marked version - * is newer than the current in storage. - */ - ConfigurationOption RESET_NEWER_DEFAULTS = ConfigurationOption.of(true); - -} diff --git a/features/versioned/src/main/java/cc/carm/lib/configuration/commentable/VersionedMetaTypes.java b/features/versioned/src/main/java/cc/carm/lib/configuration/versioned/VersionedMetaTypes.java similarity index 53% rename from features/versioned/src/main/java/cc/carm/lib/configuration/commentable/VersionedMetaTypes.java rename to features/versioned/src/main/java/cc/carm/lib/configuration/versioned/VersionedMetaTypes.java index b730606..c64135f 100644 --- a/features/versioned/src/main/java/cc/carm/lib/configuration/commentable/VersionedMetaTypes.java +++ b/features/versioned/src/main/java/cc/carm/lib/configuration/versioned/VersionedMetaTypes.java @@ -1,4 +1,4 @@ -package cc.carm.lib.configuration.commentable; +package cc.carm.lib.configuration.versioned; import cc.carm.lib.configuration.annotation.ConfigVersion; import cc.carm.lib.configuration.source.ConfigurationHolder; @@ -19,7 +19,18 @@ public interface VersionedMetaTypes { } static void register(@NotNull ConfigurationInitializer initializer) { - initializer.registerFieldAnnotation(ConfigVersion.class, VERSION, ConfigVersion::value); + initializer.appendFieldInitializer((holder, path, field, value) -> { + ConfigVersion annotation = field.getAnnotation(ConfigVersion.class); + if (annotation == null || value == null) return; + int currentVersion = annotation.value(); + int savedVersion = holder.metadata(path).get(VERSION, 0); + if (currentVersion == savedVersion) return; + + if (currentVersion > savedVersion) { // This values updated. + value.setDefault(true); // Mark as default value, force write. + } + holder.metadata(path).set(VERSION, currentVersion); + }); } } diff --git a/pom.xml b/pom.xml index 282e37e..e29d57d 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ providers/yaml providers/gson - + providers/sql providers/mongodb demo diff --git a/providers/sql/README.md b/providers/sql/README.md index 6724c5a..d4dd566 100644 --- a/providers/sql/README.md +++ b/providers/sql/README.md @@ -6,16 +6,16 @@ SQL database implementation, support for MySQL or MariaDB. ```mysql CREATE TABLE IF NOT EXISTS conf ( - `namespace` VARCHAR(32) NOT NULL, # 命名空间 (代表其属于谁,类似于单个配置文件地址的概念) - `path` VARCHAR(96) NOT NULL, # 配置路径 (ConfigPath) - `type` TINYINT UNSIGNED NOT NULL DEFAULT 0, # 数据类型 (Integer/Byte/List/Map/...) - `value` MEDIUMTEXT, # 配置项的值 (可能为JSON格式) - `inline_comment` TEXT, # 配置项的用法,本质是行内注释 - `header_comment` MEDIUMTEXT, # 配置项的描述,本质是顶部注释 - `footer_comment` MEDIUMTEXT, # 配置项的描述,本质是顶部注释 - `version` INT UNSIGNED NOT NULL DEFAULT 0, # 配置项的版本 - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, # 创建时间 - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `namespace` VARCHAR(32) NOT NULL, # 命名空间 (代表其属于谁,类似于单个配置文件地址的概念) + `path` VARCHAR(96) NOT NULL, # 配置路径 (ConfigPath) + `type` TINYINT UNSIGNED NOT NULL DEFAULT 0, # 数据类型 (Integer/Byte/List/Map/...) + `value` MEDIUMTEXT, # 配置项的值 (可能为JSON格式) + `inline_comment` TEXT comment 'usage', # 配置项的用法,本质是行内注释 + `header_comment` MEDIUMTEXT comment 'description', # 配置项的描述,本质是顶部注释 + `footer_comment` MEDIUMTEXT comment 'example', # 配置项的参考,本质是底部注释 + `version` MEDIUMINT UNSIGNED NOT NULL DEFAULT 0, # 配置项的版本 + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, # 创建时间 + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`namespace`, `path`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; diff --git a/providers/sql/pom.xml b/providers/sql/pom.xml index cc0f363..3aa1bf0 100644 --- a/providers/sql/pom.xml +++ b/providers/sql/pom.xml @@ -6,7 +6,7 @@ easyconfiguration-parent cc.carm.lib - 4.0.0 + 4.0.8 ../../pom.xml @@ -15,7 +15,8 @@ UTF-8 UTF-8 - 2.24.3 + 8.0.26 + 2.24.3 easyconfiguration-sql @@ -77,21 +78,35 @@ org.apache.logging.log4j log4j-api - ${log4j.version} + ${deps.log4j.version} test org.apache.logging.log4j log4j-core - ${log4j.version} + ${deps.log4j.version} test org.apache.logging.log4j log4j-slf4j-impl - ${log4j.version} + ${deps.log4j.version} + test + + + + ${project.parent.groupId} + easyconfiguration-gson + ${project.parent.version} + test + + + + mysql + mysql-connector-java + ${deps.mysql-driver.version} test diff --git a/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLConfigFactory.java b/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLConfigFactory.java new file mode 100644 index 0000000..bc3c37b --- /dev/null +++ b/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLConfigFactory.java @@ -0,0 +1,146 @@ +package cc.carm.lib.configuration.source.sql; + +import cc.carm.lib.configuration.adapter.ValueType; +import cc.carm.lib.configuration.commentable.Commentable; +import cc.carm.lib.configuration.function.DataFunction; +import cc.carm.lib.configuration.source.ConfigurationFactory; +import cc.carm.lib.configuration.source.ConfigurationHolder; +import cc.carm.lib.easysql.api.SQLManager; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; + +import java.util.HashMap; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class SQLConfigFactory extends ConfigurationFactory, SQLConfigFactory> { + + public static SQLConfigFactory from(@NotNull Supplier<@NotNull SQLManager> managerSupplier) { + return new SQLConfigFactory(managerSupplier); + } + + public static SQLConfigFactory from(@NotNull SQLManager manager) { + return from(() -> manager); + } + + protected @NotNull Supplier managerSupplier; + protected Supplier gsonSupplier = () -> SQLSource.DEFAULT_GSON; + + protected HashMap> resolvers = new HashMap<>(SQLValueResolver.STANDARD_RESOLVERS); + protected String tableName = "configs"; + protected String namespace = "default"; + + public SQLConfigFactory(@NotNull Supplier managerSupplier) { + this.managerSupplier = managerSupplier; + } + + @Override + protected SQLConfigFactory self() { + return this; + } + + public SQLConfigFactory manager(@NotNull Supplier managerSupplier) { + this.managerSupplier = managerSupplier; + return self(); + } + + public SQLConfigFactory manager(@NotNull SQLManager manager) { + return manager(() -> manager); + } + + public SQLConfigFactory gson(@NotNull Supplier gsonSupplier) { + this.gsonSupplier = gsonSupplier; + return self(); + } + + public SQLConfigFactory gson(@NotNull Consumer builder) { + return gson(() -> { + GsonBuilder gsonBuilder = new GsonBuilder(); + builder.accept(gsonBuilder); + return gsonBuilder.create(); + }); + } + + public SQLConfigFactory gson(@NotNull Gson gson) { + return gson(() -> gson); + } + + public SQLConfigFactory resolver(@Range(from = 0, to = 255) int type, @NotNull SQLValueResolver resolver) { + this.resolvers.put(type, resolver); + return self(); + } + + public SQLConfigFactory resolver(@Range(from = 0, to = 255) int id, @NotNull Class clazz, + @NotNull DataFunction parser) { + return resolver(id, ValueType.of(clazz), parser); + } + + public SQLConfigFactory resolver(@Range(from = 0, to = 255) int id, @NotNull ValueType type, + @NotNull DataFunction parser) { + return resolver(id, new SQLValueResolver(type) { + @Override + public @NotNull T resolve(@NotNull ConfigurationHolder holder, String data) throws Exception { + return parser.handle(data); + } + }); + } + + public SQLConfigFactory resolver(@Range(from = 0, to = 255) int id, @NotNull Class clazz, + @NotNull DataFunction parser, + @NotNull DataFunction serializer) { + return resolver(id, ValueType.of(clazz), parser, serializer); + } + + public SQLConfigFactory resolver(@Range(from = 0, to = 255) int id, @NotNull ValueType type, + @NotNull DataFunction parser, + @NotNull DataFunction serializer) { + return resolver(id, new SQLValueResolver(type) { + @Override + public @NotNull T resolve(@NotNull ConfigurationHolder holder, String data) throws Exception { + return parser.handle(data); + } + + @Override + public @NotNull String serialize(@NotNull ConfigurationHolder holder, Object value) throws Exception { + return serializer.handle(value); + } + }); + } + + public SQLConfigFactory tableName(@NotNull String tableName) { + this.tableName = tableName; + return self(); + } + + public SQLConfigFactory namespace(@NotNull String namespace) { + this.namespace = namespace; + return self(); + } + + @Override + public @NotNull ConfigurationHolder build() { + Gson gson = gsonSupplier.get(); + if (gson == null) throw new NullPointerException("No Gson instance provided."); + + SQLManager manager = this.managerSupplier.get(); + if (manager == null) throw new NullPointerException("No SQLManager instance provided."); + + Commentable.registerMeta(this.initializer); + + return new ConfigurationHolder(this.adapters, this.options, this.metadata, this.initializer) { + final SQLSource source = new SQLSource( + this, System.currentTimeMillis(), + gson, manager, resolvers, tableName, namespace + ); + + @Override + public @NotNull SQLSource config() { + return source; + } + }; + } + + +} diff --git a/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLSource.java b/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLSource.java index 4307766..1ae67bf 100644 --- a/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLSource.java +++ b/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLSource.java @@ -1,53 +1,63 @@ package cc.carm.lib.configuration.source.sql; +import cc.carm.lib.configuration.adapter.ValueType; +import cc.carm.lib.configuration.commentable.Commentable; import cc.carm.lib.configuration.source.ConfigurationHolder; import cc.carm.lib.configuration.source.section.ConfigureSource; -import cc.carm.lib.configuration.source.section.MemorySection; +import cc.carm.lib.configuration.source.section.SourcedSection; +import cc.carm.lib.configuration.value.ConfigValue; +import cc.carm.lib.configuration.versioned.VersionedMetaTypes; import cc.carm.lib.easysql.api.SQLManager; +import cc.carm.lib.easysql.api.SQLQuery; import cc.carm.lib.easysql.api.SQLTable; import cc.carm.lib.easysql.api.enums.IndexType; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.sql.ResultSet; +import java.time.LocalDateTime; import java.util.*; -public class SQLSource extends ConfigureSource, SQLSource> { +public class SQLSource extends ConfigureSource, SQLSource> { protected static final @NotNull Gson DEFAULT_GSON = new GsonBuilder() - .serializeNulls().disableHtmlEscaping().setPrettyPrinting() - .create(); + .serializeNulls().disableHtmlEscaping().create(); protected final @NotNull Gson gson; protected final @NotNull SQLManager sqlManager; protected final @NotNull String namespace; protected final @NotNull SQLTable table; - protected final @NotNull Set updated = new HashSet<>(); - protected MemorySection rootSection; + protected final @NotNull Map> resolvers; + protected SourcedSection rootSection; public SQLSource(@NotNull ConfigurationHolder holder, long lastUpdateMillis, - @NotNull Gson gson, @NotNull SQLManager sqlManager, @NotNull String tableName, @NotNull String namespace) { + @NotNull Gson gson, @NotNull SQLManager sqlManager, + @NotNull Map> resolvers, + @NotNull String tableName, @NotNull String namespace) { super(holder, lastUpdateMillis); this.gson = gson; this.sqlManager = sqlManager; + this.resolvers = resolvers; this.namespace = namespace; this.table = SQLTable.of(tableName, builder -> { builder.addColumn("namespace", "VARCHAR(32) NOT NULL"); builder.addColumn("path", "VARCHAR(96) NOT NULL"); - builder.addColumn("type", "TINYINT NOT NULL DEFAULT 0"); builder.addColumn("value", "TEXT"); builder.addColumn("inline_comment", "TEXT"); builder.addColumn("header_comments", "MEDIUMTEXT"); builder.addColumn("footer_comments", "MEDIUMTEXT"); + builder.addColumn("type", "TINYINT NOT NULL DEFAULT 0"); builder.addColumn("version", "MEDIUMINT UNSIGNED NOT NULL DEFAULT 0"); - builder.addColumn("create_time", "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"); - builder.addColumn("update_time", "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"); + builder.addColumn("create_time", "TIMESTAMP NOT NULL"); + builder.addColumn("update_time", "TIMESTAMP NOT NULL"); builder.setIndex( IndexType.PRIMARY_KEY, "pk_" + tableName.toLowerCase(), @@ -55,82 +65,131 @@ public class SQLSource extends ConfigureSource, SQLSour ); builder.setTableSettings("ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); }); + + try { + this.table.create(this.sqlManager); + onReload(); + } catch (Exception e) { + e.printStackTrace(); + } } @Override - protected SQLSource self() { + protected @NotNull SQLSource self() { return this; } + public @NotNull Gson gson() { + return gson; + } + @Override - public @NotNull Map original() { + public @NotNull Map original() { return section().data(); } @Override - public @NotNull MemorySection section() { + public @NotNull SourcedSection section() { return Objects.requireNonNull(this.rootSection, "Root section is not initialized."); } @Override public void save() throws Exception { - if (this.updated.isEmpty()) return; // Nothing to save + LocalDateTime time = LocalDateTime.now(); // Update time + List dataValues = new ArrayList<>(); - Date date = new Date(); // Update time - List values = new ArrayList<>(); + SourcedSection section = section(); + Map> values = holder().registeredValues(); + for (Map.Entry> entry : values.entrySet()) { + @NotNull String path = entry.getKey(); + @NotNull ConfigValue value = entry.getValue(); - for (String path : this.updated) { - Object value = get(path); + try { + int typeID = typeOf(entry.getValue()); + String data = null; -// if (value instanceof SQLConfigWrapper) { -// value = getSourceMap(((SQLConfigWrapper) value).getSource()); -// } -// -// SQLValueResolver type = SQLValueTypes.get(value.getClass()); -// if (type != null) { -// values.add(new Object[]{ -// this.namespace, path, date, -// type.getID(), type.serializeObject(value), -// getComments().getInlineComment(path), -// GSON.toJson(getComments().getHeaderComment(path)) -// }); -// } + if (value != null) { + if (value instanceof SourcedSection) { + data = serialize(typeID, ((SourcedSection) value).rawMap()); + } else if (value instanceof List) { + List list = new ArrayList<>(); + for (Object obj : (List) value) { + if (obj instanceof SourcedSection) { + list.add(((SourcedSection) obj).rawMap()); + } else { + list.add(obj); + } + } + data = serialize(typeID, list); + } else { + data = serialize(typeID, value); + } + } + + int version = holder().metadata(path).get(VersionedMetaTypes.VERSION, 0); + dataValues.add(new Object[]{ + namespace, path, time, version, typeID, data, + Commentable.getInlineComment(holder(), path), + gson.toJson(Commentable.getHeaderComments(holder(), path)), + gson.toJson(Commentable.getFooterComments(holder(), path)) + }); + } catch (Exception ex) { + ex.printStackTrace(); + } } - this.updated.clear(); - this.table.createReplaceBatch() - .setColumnNames("namespace", "path", "update_time", "type", "value", "inline_comment", "header_comments") - .setAllParams(values) - .execute(); + .setColumnNames( + "namespace", "path", "update_time", "version", "type", "value", + "inline_comment", "header_comments", "footer_comments" + ).setAllParams(dataValues).execute(); } @Override protected void onReload() throws Exception { - LinkedHashMap values = new LinkedHashMap<>(); - -// try (SQLQuery query = this.table.createQuery() -// .addCondition("namespace", namespace) -// .build().execute()) { -// ResultSet rs = query.getResultSet(); -// while (rs.next()) { -// String path = rs.getString("path"); -// int type = rs.getInt("type"); -// try { -// SQLValueResolver resolver = SQLValueTypes.get(type); -// if (resolver == null) throw new IllegalStateException("No resolver for type #" + type); -// String value = rs.getString("value"); -// values.put(path, resolver.resolve(value)); -// -// loadInlineComment(path, rs.getString("inline_comment")); -// loadHeaderComment(path, rs.getString("header_comments")); -// } catch (Exception ex) { -// ex.printStackTrace(); -// } -// } -// } -// -// this.rootConfiguration = new SQLConfigWrapper(this, values); + Map loaded = new LinkedHashMap<>(); + try (SQLQuery query = this.table.createQuery() + .addCondition("namespace", namespace) + .build().execute()) { + ResultSet rs = query.getResultSet(); + while (rs.next()) { + String path = rs.getString("path"); + if (path == null) continue; // Path should be not null + int ver = rs.getInt("version"); + try { + Object val = parse(rs.getInt("type"), rs.getString("value")); + System.out.println("Path <" + path + "> Value = " + val); + loaded.put(path, val); + if (ver != 0) { + holder().metadata(path).set(VersionedMetaTypes.VERSION, ver); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + this.rootSection = SourcedSection.root(this, loaded); } + + protected @Nullable Object parse(int type, String value) throws Exception { + SQLValueResolver function = this.resolvers.get(type); + if (function == null) throw new IllegalStateException("No resolvers for type #" + type); + return function.resolve(holder(), value); + } + + protected @Nullable String serialize(int type, @NotNull Object value) throws Exception { + SQLValueResolver function = this.resolvers.get(type); + if (function == null) throw new IllegalStateException("No resolvers for type #" + type); + return function.serialize(holder(), value); + } + + protected int typeOf(@NotNull ValueType value) { + return this.resolvers.entrySet().stream() + .filter(entry -> entry.getValue().isTypeOf(value)) + .findFirst().map(Map.Entry::getKey) + .orElseThrow(() -> new IllegalStateException("No resolvers for value " + value)); + } + + } diff --git a/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLValueResolver.java b/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLValueResolver.java new file mode 100644 index 0000000..706bd04 --- /dev/null +++ b/providers/sql/src/main/java/cc/carm/lib/configuration/source/sql/SQLValueResolver.java @@ -0,0 +1,103 @@ +package cc.carm.lib.configuration.source.sql; + +import cc.carm.lib.configuration.adapter.ValueType; +import cc.carm.lib.configuration.function.DataFunction; +import cc.carm.lib.configuration.source.ConfigurationHolder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class SQLValueResolver { + + public static final @NotNull SQLValueResolver STRING = SQLValueResolver.of(ValueType.STRING, s -> s); + public static final @NotNull SQLValueResolver BYTE = SQLValueResolver.of(ValueType.BYTE, Byte::parseByte); + public static final @NotNull SQLValueResolver SHORT = SQLValueResolver.of(ValueType.SHORT, Short::parseShort); + public static final @NotNull SQLValueResolver INTEGER = SQLValueResolver.of(ValueType.INTEGER, Integer::parseInt); + public static final @NotNull SQLValueResolver LONG = SQLValueResolver.of(ValueType.LONG, Long::parseLong); + public static final @NotNull SQLValueResolver FLOAT = SQLValueResolver.of(ValueType.FLOAT, Float::parseFloat); + public static final @NotNull SQLValueResolver DOUBLE = SQLValueResolver.of(ValueType.DOUBLE, Double::parseDouble); + public static final @NotNull SQLValueResolver BOOLEAN = SQLValueResolver.of(ValueType.BOOLEAN, Boolean::parseBoolean); + public static final @NotNull SQLValueResolver CHAR = SQLValueResolver.of(ValueType.CHAR, s -> s.charAt(0)); + + public static final @NotNull SQLValueResolver> LIST = new SQLValueResolver>(new ValueType>() { + }) { + @Override + public @Nullable List resolve(@NotNull ConfigurationHolder holder, String data) throws Exception { + return holder.config().gson().fromJson(data, List.class); + } + + @Override + public @Nullable String serialize(@NotNull ConfigurationHolder holder, Object value) { + return holder.config().gson().toJson(value); + } + }; + + public static final @NotNull SQLValueResolver> MAP = new SQLValueResolver>(new ValueType>() { + }) { + @Override + public @Nullable Map resolve(@NotNull ConfigurationHolder holder, String data) throws Exception { + return holder.config().gson().fromJson(data, LinkedHashMap.class); + } + + @Override + public @Nullable String serialize(@NotNull ConfigurationHolder holder, Object value) { + return holder.config().gson().toJson(value); + } + }; + + public static final @NotNull Map> STANDARD_RESOLVERS = Collections.unmodifiableMap(standards()); + + static Map> standards() { + Map> map = new LinkedHashMap<>(); + map.put(0, STRING); + map.put(1, BYTE); + map.put(2, SHORT); + map.put(3, INTEGER); + map.put(4, LONG); + map.put(5, FLOAT); + map.put(6, DOUBLE); + map.put(7, BOOLEAN); + map.put(8, CHAR); + map.put(10, LIST); + map.put(11, MAP); + return map; + } + + public static SQLValueResolver of(@NotNull ValueType type, @NotNull DataFunction resolver) { + return new SQLValueResolver(type) { + @Override + public @NotNull V resolve(@NotNull ConfigurationHolder holder, String data) throws Exception { + return resolver.handle(data); + } + }; + } + + protected final @NotNull ValueType type; + + protected SQLValueResolver(@NotNull ValueType type) { + this.type = type; + } + + public @NotNull ValueType getType() { + return type; + } + + public boolean isTypeOf(@NotNull Class clazz) { + return type.isSubtypeOf(clazz); + } + + public boolean isTypeOf(@NotNull ValueType valueType) { + return valueType.equals(type); + } + + public abstract @Nullable T resolve(@NotNull ConfigurationHolder holder, String data) throws Exception; + + public @Nullable String serialize(@NotNull ConfigurationHolder holder, Object value) throws Exception { + return String.valueOf(value); + } + +} diff --git a/providers/sql/src/test/java/config/SQLConfigTest.java b/providers/sql/src/test/java/config/SQLConfigTest.java index ca569f8..e73d2a7 100644 --- a/providers/sql/src/test/java/config/SQLConfigTest.java +++ b/providers/sql/src/test/java/config/SQLConfigTest.java @@ -1,27 +1,45 @@ package config; +import cc.carm.lib.configuration.demo.DatabaseConfiguration; +import cc.carm.lib.configuration.demo.tests.ConfigurationTest; +import cc.carm.lib.configuration.source.ConfigurationHolder; +import cc.carm.lib.configuration.source.json.JSONConfigFactory; +import cc.carm.lib.configuration.source.sql.SQLConfigFactory; import cc.carm.lib.easysql.EasySQL; import cc.carm.lib.easysql.api.SQLManager; import cc.carm.lib.easysql.beecp.BeeDataSourceConfig; +import org.junit.Test; + +import java.io.File; public class SQLConfigTest { - + @Test public void test() { + + ConfigurationHolder gsonHolder = JSONConfigFactory.from(new File("target/sql.json")).build(); + gsonHolder.initialize(DatabaseConfiguration.class); + BeeDataSourceConfig config = new BeeDataSourceConfig(); - config.setDriverClassName("org.h2.Driver"); - config.setJdbcUrl("jdbc:h2:file:target/test;DB_CLOSE_DELAY=-1;MODE=MySQL;"); + config.setDriverClassName(DatabaseConfiguration.DRIVER_NAME.resolve()); + config.setJdbcUrl(DatabaseConfiguration.buildJDBC()); + config.setUsername(DatabaseConfiguration.USERNAME.resolve()); + config.setPassword(DatabaseConfiguration.PASSWORD.resolve()); + SQLManager manager = EasySQL.createManager(config); manager.setDebugMode(true); -// SQLConfigProvider provider = EasyConfiguration.from(manager, "conf_test", "TESTING"); -// -// ConfigurationTest.testDemo(provider); -// ConfigurationTest.testInner(provider); -// -// ConfigurationTest.save(provider); -// -// EasySQL.shutdownManager(manager); + ConfigurationHolder holder = SQLConfigFactory.from(manager) + .tableName("test_configs") + .namespace("testing") + .build(); + + ConfigurationTest.testDemo(holder); + ConfigurationTest.testInner(holder); + + ConfigurationTest.save(holder); + + EasySQL.shutdownManager(manager); } }