diff --git a/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/PrimitiveAdapter.java b/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/PrimitiveAdapter.java index ecd3dd5..f0c32a7 100644 --- a/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/PrimitiveAdapter.java +++ b/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/PrimitiveAdapter.java @@ -10,12 +10,6 @@ import java.util.Arrays; public class PrimitiveAdapter extends ValueAdapter { - public static final PrimitiveAdapter[] ADAPTERS = new PrimitiveAdapter[]{ - ofString(), ofBoolean(), ofBooleanType(), ofCharacter(), ofCharacterType(), - ofInteger(), ofIntegerType(), ofLong(), ofLongType(), ofDouble(), ofDoubleType(), - ofFloat(), ofFloatType(), ofShort(), ofShortType(), ofByte(), ofByteType() - }; - public static final String[] TRUE_VALUES = new String[]{ "true", "yes", "on", "1", "enabled", "enable", "active" }; diff --git a/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/StandardAdapters.java b/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/StandardAdapters.java index 4b332d5..a0d3435 100644 --- a/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/StandardAdapters.java +++ b/core/src/main/java/cc/carm/lib/configuration/adapter/strandard/StandardAdapters.java @@ -3,10 +3,21 @@ package cc.carm.lib.configuration.adapter.strandard; import cc.carm.lib.configuration.adapter.ValueAdapter; import cc.carm.lib.configuration.adapter.ValueType; import cc.carm.lib.configuration.source.section.ConfigureSection; +import org.jetbrains.annotations.NotNull; + +import static cc.carm.lib.configuration.adapter.strandard.PrimitiveAdapter.*; public interface StandardAdapters { - ValueAdapter SECTION_ADAPTER = new ValueAdapter<>( + @NotNull PrimitiveAdapter[] PRIMITIVES = new PrimitiveAdapter[]{ + ofString(), ofBoolean(), ofBooleanType(), ofCharacter(), ofCharacterType(), + ofInteger(), ofIntegerType(), ofLong(), ofLongType(), ofDouble(), ofDoubleType(), + ofFloat(), ofFloatType(), ofShort(), ofShortType(), ofByte(), ofByteType() + }; + + @NotNull ValueAdapter> ENUMS = PrimitiveAdapter.ofEnum(); + + @NotNull ValueAdapter SECTIONS = new ValueAdapter<>( ValueType.of(ConfigureSection.class), (provider, type, value) -> value, (provider, type, value) -> { diff --git a/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationFactory.java b/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationFactory.java index ceabbef..85d466e 100644 --- a/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationFactory.java +++ b/core/src/main/java/cc/carm/lib/configuration/source/ConfigurationFactory.java @@ -1,18 +1,21 @@ package cc.carm.lib.configuration.source; import cc.carm.lib.configuration.adapter.*; -import cc.carm.lib.configuration.adapter.strandard.PrimitiveAdapter; import cc.carm.lib.configuration.adapter.strandard.StandardAdapters; import cc.carm.lib.configuration.function.DataFunction; import cc.carm.lib.configuration.source.loader.ConfigurationInitializer; import cc.carm.lib.configuration.source.loader.PathGenerator; +import cc.carm.lib.configuration.source.meta.ConfigurationMetaHolder; import cc.carm.lib.configuration.source.meta.ConfigurationMetadata; 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -25,11 +28,13 @@ public abstract class ConfigurationFactory< protected ValueAdapterRegistry adapters = new ValueAdapterRegistry(); protected ConfigurationOptionHolder options = new ConfigurationOptionHolder(); + protected @NotNull Map metadata = new HashMap<>(); protected ConfigurationInitializer initializer = new ConfigurationInitializer(); public ConfigurationFactory() { - this.adapters.register(PrimitiveAdapter.ADAPTERS); - this.adapters.register(StandardAdapters.SECTION_ADAPTER); + this.adapters.register(StandardAdapters.PRIMITIVES); + this.adapters.register(StandardAdapters.SECTIONS); + this.adapters.register(StandardAdapters.ENUMS); } protected abstract SELF self(); @@ -102,6 +107,27 @@ public abstract class ConfigurationFactory< }); } + public SELF metadata(@NotNull Map metadata) { + this.metadata = metadata; + return self(); + } + + public SELF metadata(@NotNull Consumer> handler) { + handler.accept(this.metadata); + return self(); + } + + public SELF metadata(@Nullable String path, @NotNull ConfigurationMetaHolder meta) { + return metadata(m -> m.put(path, meta)); + } + + public SELF metadata(@Nullable String path, @NotNull Consumer handler) { + return metadata(map -> { + ConfigurationMetaHolder meta = map.computeIfAbsent(path, k -> new ConfigurationMetaHolder()); + handler.accept(meta); + }); + } + public SELF initializer(ConfigurationInitializer initializer) { this.initializer = initializer; 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 94592ab..851c846 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 @@ -5,13 +5,17 @@ import cc.carm.lib.configuration.adapter.ValueAdapterRegistry; 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.option.ConfigurationOptionHolder; import cc.carm.lib.configuration.source.section.ConfigureSource; import cc.carm.lib.configuration.value.ValueManifest; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; public abstract class ConfigurationHolder> { @@ -54,6 +58,16 @@ public abstract class ConfigurationHolder new ConfigurationMetaHolder()); } + @NotNull + @UnmodifiableView + public Map extractMetadata(@NotNull ConfigurationMetadata type) { + 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); + } + return Collections.unmodifiableMap(metas); + } public ValueAdapterRegistry adapters() { return this.adapters; diff --git a/core/src/main/java/cc/carm/lib/configuration/value/ConfigValue.java b/core/src/main/java/cc/carm/lib/configuration/value/ConfigValue.java index 7b7075a..b9fe6c0 100644 --- a/core/src/main/java/cc/carm/lib/configuration/value/ConfigValue.java +++ b/core/src/main/java/cc/carm/lib/configuration/value/ConfigValue.java @@ -72,7 +72,7 @@ public abstract class ConfigValue extends ValueManifest { * * @return Non-null value * @throws NullPointerException Thrown when the corresponding data is null - * @see #resolve() + * @see #resolve() for a more descriptive function */ public @NotNull T getNotNull() { return resolve(); @@ -114,7 +114,7 @@ public abstract class ConfigValue extends ValueManifest { */ public void setDefault(boolean override) { if (!override && config().contains(path())) return; - Optional.ofNullable(defaults()).ifPresent(this::set); + set(defaults()); // Set the default value } /** 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 f35e99c..02424b3 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 @@ -2,15 +2,15 @@ package cc.carm.lib.configuration.demo; import cc.carm.lib.configuration.Configuration; import cc.carm.lib.configuration.annotation.ConfigPath; -import cc.carm.lib.configuration.annotation.HeaderComment; +import cc.carm.lib.configuration.annotation.HeaderComments; import cc.carm.lib.configuration.value.ConfigValue; import cc.carm.lib.configuration.value.standard.ConfiguredValue; -@HeaderComment({"", "数据库配置", " 用于提供数据库连接,进行数据库操作。"}) +@HeaderComments({"", "数据库配置", " 用于提供数据库连接,进行数据库操作。"}) public class DatabaseConfiguration implements Configuration { @ConfigPath("driver") - @HeaderComment({ + @HeaderComments({ "数据库驱动配置,请根据数据库类型设置。", "- MySQL(旧): com.mysql.jdbc.Driver", "- MySQL(新): com.mysql.cj.jdbc.Driver", diff --git a/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/DemoConfiguration.java b/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/DemoConfiguration.java index 8623ac8..5190941 100644 --- a/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/DemoConfiguration.java +++ b/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/DemoConfiguration.java @@ -2,7 +2,8 @@ package cc.carm.lib.configuration.demo.tests.conf; import cc.carm.lib.configuration.Configuration; import cc.carm.lib.configuration.annotation.ConfigPath; -import cc.carm.lib.configuration.annotation.HeaderComment; +import cc.carm.lib.configuration.annotation.FooterComments; +import cc.carm.lib.configuration.annotation.HeaderComments; import cc.carm.lib.configuration.annotation.InlineComment; import cc.carm.lib.configuration.demo.DatabaseConfiguration; import cc.carm.lib.configuration.demo.tests.model.UserRecord; @@ -14,16 +15,20 @@ import java.time.temporal.ChronoUnit; import java.util.Objects; import java.util.UUID; - -@HeaderComment({"此处内容将显示在配置文件的最上方"}) +@ConfigPath(root = true) +@HeaderComments({"此处内容将显示在配置文件的最上方"}) +@FooterComments({"此处内容将显示在配置文件的最下方", "可用于显示版权信息等"}) public interface DemoConfiguration extends Configuration { @ConfigPath(root = true) ConfigValue VERSION = ConfiguredValue.of(Double.class, 1.0D); @ConfigPath(root = true) + @FooterComments({"此处内容将显示在配置条目的下方", "可用于补充说明,但一般不建议使用"}) ConfigValue TEST_NUMBER = ConfiguredValue.of(1000000L); + @HeaderComments({"枚举类型测试"}) + @FooterComments({"上述的枚举内容本质上是通过STRING解析的"}) ConfigValue TEST_ENUM = ConfiguredValue.of(ChronoUnit.class, ChronoUnit.DAYS); // 支持通过 Class 变量标注子配置,一并注册。 @@ -31,8 +36,10 @@ public interface DemoConfiguration extends Configuration { Class DATABASE = DatabaseConfiguration.class; @ConfigPath("registered_users") // 通过注解规定配置文件中的路径,若不进行注解则以变量名自动生成。 - @HeaderComment({"Section类型数据测试"}) // 通过注解给配置添加注释。 - @InlineComment("Section数据也支持InlineComment注释") + @HeaderComments({"Section类型数据测试"}) // 通过注解给配置添加注释。 + @InlineComment("默认地注释会加到Section的首行末尾") // 通过注解给配置添加注释。 + @InlineComment(value = "用户名(匹配注释)", regex = "name") // 通过注解给配置添加注释。 + @InlineComment(value = "信息", regex = "info.*") // 通过注解给配置添加注释。 ConfiguredList USERS = ConfiguredList.builderOf(UserRecord.class).fromSection() .parse(UserRecord::deserialize).serialize(UserRecord::serialize) .defaults(UserRecord.CARM).build(); @@ -52,15 +59,15 @@ public interface DemoConfiguration extends Configuration { class SUB implements Configuration { @ConfigPath(value = "uuid-value", root = true) - @InlineComment("This is an inline comment") public static final ConfigValue UUID_CONFIG_VALUE = ConfiguredValue .builderOf(UUID.class).fromString() .parse((holder, data) -> UUID.fromString(data)) .build(); - public static class That implements Configuration { + @HeaderComments({"内部类的内部类测试", "通过这种方式,您可以轻易实现多层次的配置文件结构"}) + public interface That extends Configuration { - public static final ConfiguredList OPERATORS = ConfiguredList + ConfiguredList OPERATORS = ConfiguredList .builderOf(UUID.class).fromString() .parse(s -> Objects.requireNonNull(UUID.fromString(s))) .build(); diff --git a/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/InstanceConfig.java b/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/InstanceConfig.java index c0d7a8b..034db9a 100644 --- a/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/InstanceConfig.java +++ b/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/InstanceConfig.java @@ -1,11 +1,11 @@ package cc.carm.lib.configuration.demo.tests.conf; import cc.carm.lib.configuration.Configuration; -import cc.carm.lib.configuration.annotation.HeaderComment; +import cc.carm.lib.configuration.annotation.HeaderComments; import cc.carm.lib.configuration.value.ConfigValue; import cc.carm.lib.configuration.value.standard.ConfiguredValue; -@HeaderComment("Inner Test") +@HeaderComments("Inner Test") public class InstanceConfig implements Configuration { public final ConfigValue INNER_VALUE = ConfiguredValue.of(1.0D); diff --git a/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/RegistryConfig.java b/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/RegistryConfig.java index 36fceca..48c0df7 100644 --- a/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/RegistryConfig.java +++ b/demo/src/main/java/cc/carm/lib/configuration/demo/tests/conf/RegistryConfig.java @@ -2,7 +2,8 @@ package cc.carm.lib.configuration.demo.tests.conf; import cc.carm.lib.configuration.Configuration; import cc.carm.lib.configuration.annotation.ConfigPath; -import cc.carm.lib.configuration.annotation.HeaderComment; +import cc.carm.lib.configuration.annotation.FooterComments; +import cc.carm.lib.configuration.annotation.HeaderComments; import cc.carm.lib.configuration.annotation.InlineComment; import cc.carm.lib.configuration.demo.tests.model.UserRecord; import cc.carm.lib.configuration.value.ConfigValue; @@ -12,12 +13,11 @@ import java.util.UUID; public class RegistryConfig implements Configuration { - @HeaderComment("Support for configurations as instances") + @HeaderComments("Support for configurations as instances") public final InstanceConfig INSTANCE = new InstanceConfig(); @ConfigPath("test.user") // 通过注解规定配置文件中的路径,若不进行注解则以变量名自动生成。 - @HeaderComment({"Section类型数据测试"}) // 通过注解给配置添加注释。 - @InlineComment("Section数据也支持InlineComment注释") + @FooterComments({"12313213212"}) public final ConfigValue TEST_MODEL = ConfiguredValue.builderOf(UserRecord.class).fromSection() .defaults(new UserRecord("Carm", UUID.randomUUID())) .parse((holder, section) -> UserRecord.deserialize(section)) diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/FooterComment.java b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/FooterComments.java similarity index 96% rename from features/commentable/src/main/java/cc/carm/lib/configuration/annotation/FooterComment.java rename to features/commentable/src/main/java/cc/carm/lib/configuration/annotation/FooterComments.java index 24971d6..ac42c70 100644 --- a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/FooterComment.java +++ b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/FooterComments.java @@ -19,7 +19,7 @@ import java.lang.annotation.Target; */ @Target({ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) -public @interface FooterComment { +public @interface FooterComments { /** * If the content of the note is 0, it will be treated as a blank line. diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/HeaderComment.java b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/HeaderComments.java similarity index 96% rename from features/commentable/src/main/java/cc/carm/lib/configuration/annotation/HeaderComment.java rename to features/commentable/src/main/java/cc/carm/lib/configuration/annotation/HeaderComments.java index 284b290..79da316 100644 --- a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/HeaderComment.java +++ b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/HeaderComments.java @@ -19,7 +19,7 @@ import java.lang.annotation.Target; */ @Target({ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) -public @interface HeaderComment { +public @interface HeaderComments { /** * If the content of the note is 0, it will be treated as a blank line. diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComment.java b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComment.java index 763f896..45c51e7 100644 --- a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComment.java +++ b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComment.java @@ -2,10 +2,7 @@ package cc.carm.lib.configuration.annotation; import org.jetbrains.annotations.NotNull; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; /** * Inline comments, @@ -17,6 +14,7 @@ import java.lang.annotation.Target; */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) +@Repeatable(InlineComments.class) public @interface InlineComment { /** @@ -35,8 +33,9 @@ public @interface InlineComment { * If the regex is not empty, the comment will be added to * all sub paths if the regex matches the value. * If the regex is empty, the comment will be added to the current path. - *

e.g. "^foo\\.*\\.bar" will be set like + *

e.g. for section, set {"^foo", "*", "bar"} will be set like *

+     *   section:
      *     foo:
      *       some:
      *         lover: "bar" <- not matched so no comments
diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComments.java b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComments.java
new file mode 100644
index 0000000..a162961
--- /dev/null
+++ b/features/commentable/src/main/java/cc/carm/lib/configuration/annotation/InlineComments.java
@@ -0,0 +1,15 @@
+package cc.carm.lib.configuration.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface InlineComments {
+
+
+    InlineComment[] value();
+
+}
diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/commentable/CommentableMeta.java b/features/commentable/src/main/java/cc/carm/lib/configuration/commentable/CommentableMeta.java
new file mode 100644
index 0000000..a036ee0
--- /dev/null
+++ b/features/commentable/src/main/java/cc/carm/lib/configuration/commentable/CommentableMeta.java
@@ -0,0 +1,67 @@
+package cc.carm.lib.configuration.commentable;
+
+import cc.carm.lib.configuration.annotation.FooterComments;
+import cc.carm.lib.configuration.annotation.HeaderComments;
+import cc.carm.lib.configuration.annotation.InlineComment;
+import cc.carm.lib.configuration.annotation.InlineComments;
+import cc.carm.lib.configuration.source.ConfigurationHolder;
+import cc.carm.lib.configuration.source.loader.ConfigurationInitializer;
+import cc.carm.lib.configuration.source.meta.ConfigurationMetadata;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+public interface CommentableMeta {
+
+    /**
+     * Configuration's {@link HeaderComments}
+     *
+     * @see HeaderComments
+     */
+    ConfigurationMetadata> HEADER = ConfigurationMetadata.of(Collections.emptyList());
+
+    /**
+     * Configuration's footer comments
+     *
+     * @see FooterComments
+     */
+    ConfigurationMetadata> FOOTER = ConfigurationMetadata.of(Collections.emptyList());
+
+    /**
+     * Configuration's {@link InlineComment}
+     * 

Map< regex, comment > , regex is used to match the key, null for current path. + * + * @see InlineComment + */ + ConfigurationMetadata> INLINE = ConfigurationMetadata.of(); + + + static void register(@NotNull ConfigurationHolder provider) { + register(provider.initializer()); + } + + static void register(@NotNull ConfigurationInitializer initializer) { + initializer.registerAnnotation( + HeaderComments.class, HEADER, + a -> Arrays.asList(a.value()) + ); + initializer.registerAnnotation( + FooterComments.class, FOOTER, + a -> Arrays.asList(a.value()) + ); + initializer.registerAnnotation(InlineComments.class, INLINE, a -> { + Map map = new HashMap<>(); + for (InlineComment comment : a.value()) { + if (comment.regex().length == 0) { // for current path + map.put(null, comment.value()); + continue; + } + for (String regex : comment.regex()) { // for specified path + map.put(regex, comment.value()); + } + } + return map; + }); + } + +} diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/commentable/CommentableMetaTypes.java b/features/commentable/src/main/java/cc/carm/lib/configuration/commentable/CommentableMetaTypes.java deleted file mode 100644 index ce73428..0000000 --- a/features/commentable/src/main/java/cc/carm/lib/configuration/commentable/CommentableMetaTypes.java +++ /dev/null @@ -1,49 +0,0 @@ -package cc.carm.lib.configuration.commentable; - -import cc.carm.lib.configuration.annotation.FooterComment; -import cc.carm.lib.configuration.annotation.HeaderComment; -import cc.carm.lib.configuration.annotation.InlineComment; -import cc.carm.lib.configuration.source.ConfigurationHolder; -import cc.carm.lib.configuration.source.loader.ConfigurationInitializer; -import cc.carm.lib.configuration.source.meta.ConfigurationMetadata; -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public interface CommentableMetaTypes { - - /** - * Configuration's {@link HeaderComment} - */ - ConfigurationMetadata> HEADER_COMMENTS = ConfigurationMetadata.of(Collections.emptyList()); - - /** - * Configuration's footer comments - */ - ConfigurationMetadata> FOOTER_COMMENTS = ConfigurationMetadata.of(Collections.emptyList()); - - /** - * Configuration's {@link InlineComment} - */ - ConfigurationMetadata INLINE_COMMENT = ConfigurationMetadata.of(); - - - static void register(@NotNull ConfigurationHolder provider) { - register(provider.initializer()); - } - - static void register(@NotNull ConfigurationInitializer initializer) { - initializer.registerAnnotation( - HeaderComment.class, HEADER_COMMENTS, - a -> Arrays.asList(a.value()) - ); - initializer.registerAnnotation( - FooterComment.class, FOOTER_COMMENTS, - a -> Arrays.asList(a.value()) - ); - initializer.registerAnnotation(InlineComment.class, INLINE_COMMENT, InlineComment::value); - } - -} diff --git a/features/commentable/src/main/java/cc/carm/lib/configuration/option/CommentableOptions.java b/features/commentable/src/main/java/cc/carm/lib/configuration/option/CommentableOptions.java index 09a89e4..c903aa0 100644 --- a/features/commentable/src/main/java/cc/carm/lib/configuration/option/CommentableOptions.java +++ b/features/commentable/src/main/java/cc/carm/lib/configuration/option/CommentableOptions.java @@ -8,7 +8,7 @@ public interface CommentableOptions { // * Whether to keep modified comments in configuration, // * that means we only set comments for values that are not exists in configuration. // */ -// ConfigurationOption KEEP_COMMENTS = ConfigurationOption.of(true); +// ConfigurationOption KEEP_COMMENTS = ConfigurationOption.of(true); // TODO: Implement this feature /** * Whether to comment values name that are not exists in configuration and no default value offered. @@ -19,6 +19,6 @@ public interface CommentableOptions { * # foo: *

*/ - ConfigurationOption COMMENT_EMPTY_VALUE = ConfigurationOption.of(true); + ConfigurationOption COMMENT_EMPTY_VALUE = ConfigurationOption.of(false); } diff --git a/features/section/src/main/java/cc/carm/lib/configuration/source/section/MemorySection.java b/features/section/src/main/java/cc/carm/lib/configuration/source/section/MemorySection.java index c171629..f682d68 100644 --- a/features/section/src/main/java/cc/carm/lib/configuration/source/section/MemorySection.java +++ b/features/section/src/main/java/cc/carm/lib/configuration/source/section/MemorySection.java @@ -68,11 +68,7 @@ public class MemorySection implements ConfigureSection { @Override public @NotNull Map getValues(boolean deep) { - if (deep) { - Map values = new LinkedHashMap<>(); - mapChildrenValues(values, this, null, true); - return Collections.unmodifiableMap(values); - } else return Collections.unmodifiableMap(data()); + return Collections.unmodifiableMap(deep ? mapChildrenValues(this, null, true) : data()); } @Override @@ -81,13 +77,11 @@ public class MemorySection implements ConfigureSection { MemorySection section = getSectionFor(path); if (section == this) { - if (value == null) { - this.data.remove(path); - } else { - this.data.put(path, value); - } + // Even this value is null, we still need to put it in the map + // to ensure that the path is marked as existing. + this.data.put(path, value); } else { - section.set(getChild(path), value); + section.set(childPath(path), value); } } @@ -99,7 +93,7 @@ public class MemorySection implements ConfigureSection { @Override public @Nullable Object get(@NotNull String path) { MemorySection section = getSectionFor(path); - return section == this ? data.get(path) : section.get(getChild(path)); + return section == this ? data.get(path) : section.get(childPath(path)); } @Override @@ -138,29 +132,29 @@ public class MemorySection implements ConfigureSection { return (MemorySection) section; } - private String getChild(String path) { + private String childPath(String path) { int index = path.indexOf(separator()); return (index == -1) ? path : path.substring(index + 1); } - /** * Map the values of the children of the section to the output map. * - * @param output The map to map the values to * @param section The section to map the values from * @param parent The parent path * @param deep If the mapping should be deep */ - protected void mapChildrenValues(@NotNull Map output, @NotNull MemorySection section, - @Nullable String parent, boolean deep) { + protected Map mapChildrenValues(@NotNull MemorySection section, + @Nullable String parent, boolean deep) { + Map output = new LinkedHashMap<>(); for (Map.Entry entry : section.data().entrySet()) { String path = (parent == null ? "" : parent + separator()) + entry.getKey(); output.remove(path); output.put(path, entry.getValue()); if (deep && entry.getValue() instanceof MemorySection) { - this.mapChildrenValues(output, (MemorySection) entry.getValue(), path, true); + output.putAll(mapChildrenValues((MemorySection) entry.getValue(), path, true)); } } + return output; } } diff --git a/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java b/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java index c4e488b..83eef26 100644 --- a/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java +++ b/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java @@ -7,8 +7,6 @@ import com.google.gson.GsonBuilder; import org.jetbrains.annotations.NotNull; import java.io.File; -import java.util.HashMap; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; @@ -58,7 +56,7 @@ public class JSONConfigFactory extends FileConfigFactory(this.adapters, this.options, new HashMap<>(), this.initializer) { + return new ConfigurationHolder(this.adapters, this.options, this.metadata, this.initializer) { final JSONSource source = new JSONSource(this, configFile, sourcePath, gson); @Override diff --git a/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONSource.java b/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONSource.java index 4ab433a..abbd339 100644 --- a/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONSource.java +++ b/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONSource.java @@ -67,15 +67,15 @@ public class JSONSource extends FileConfigSource, JSONS fileWriter(writer -> gson.toJson(original(), writer)); } - public @NotNull String saveToString() { - return gson.toJson(original()); + @Override + protected void onReload() throws Exception { + Map data = fileReader(reader -> gson.fromJson(reader, LinkedHashMap.class)); + this.rootSection = MemorySection.root(this, data); + this.lastUpdateMillis = System.currentTimeMillis(); // 更新时间 } @Override - protected void onReload() throws Exception { - this.rootSection = MemorySection.root( - this, fileReader(reader -> gson.fromJson(reader, LinkedHashMap.class)) - ); - this.lastUpdateMillis = System.currentTimeMillis(); // 更新时间 + public String toString() { + return gson.toJson(original()); } } diff --git a/providers/yaml/pom.xml b/providers/yaml/pom.xml index 3e1239f..10eb5b7 100644 --- a/providers/yaml/pom.xml +++ b/providers/yaml/pom.xml @@ -14,6 +14,7 @@ ${project.jdk.version} UTF-8 UTF-8 + 1.1.0 easyconfiguration-yaml @@ -56,6 +57,13 @@ compile + + cc.carm.lib + yamlcommentwriter + ${deps.yamlcommentwriter.version} + compile + + org.yaml snakeyaml diff --git a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java index 5d063aa..47fbdf3 100644 --- a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java +++ b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java @@ -1,6 +1,6 @@ package cc.carm.lib.configuration.source.yaml; -import cc.carm.lib.configuration.commentable.CommentableMetaTypes; +import cc.carm.lib.configuration.commentable.CommentableMeta; import cc.carm.lib.configuration.source.ConfigurationHolder; import cc.carm.lib.configuration.source.file.FileConfigFactory; import org.jetbrains.annotations.NotNull; @@ -9,8 +9,7 @@ import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import java.io.File; -import java.util.HashMap; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Arrays; import java.util.function.Consumer; public class YAMLConfigFactory extends FileConfigFactory, YAMLConfigFactory> { @@ -60,17 +59,30 @@ public class YAMLConfigFactory extends FileConfigFactory This will override any existing header comments. + * + * @param header The header comments to set + * @return The current factory instance + */ + public YAMLConfigFactory header(@NotNull String... header) { + return metadata(null, holder -> holder.set(CommentableMeta.HEADER, Arrays.asList(header))); + } + + public YAMLConfigFactory footer(@NotNull String... footer) { + return metadata(null, holder -> holder.set(CommentableMeta.FOOTER, Arrays.asList(footer))); + } + @Override public @NotNull ConfigurationHolder build() { File configFile = this.file; String sourcePath = this.resourcePath; - CommentableMetaTypes.register(this.initializer); // Register commentable meta types + CommentableMeta.register(this.initializer); // Register commentable meta types - return new ConfigurationHolder( - this.adapters, this.options, new HashMap<>(), this.initializer - ) { + return new ConfigurationHolder(this.adapters, this.options, this.metadata, this.initializer) { final @NotNull YAMLSource source = new YAMLSource(this, configFile, sourcePath); @Override diff --git a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java index 55cb6e5..ee27965 100644 --- a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java +++ b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java @@ -6,24 +6,39 @@ import org.yaml.snakeyaml.LoaderOptions; public interface YAMLOptions { - + /** + * The {@link LoaderOptions} for SnakeYAML. + * + * @see LoaderOptions + */ ConfigurationOption LOADER = ConfigurationOption.of(() -> { - LoaderOptions loaderOptions = new LoaderOptions(); + LoaderOptions opt = new LoaderOptions(); + // As we handle comments ourselves, // we don't want SnakeYAML to read them when loading the configs. - loaderOptions.setProcessComments(false); - loaderOptions.setMaxAliasesForCollections(100); // 100 aliases - loaderOptions.setCodePointLimit(5 * 1024 * 1024); // 5MB - return loaderOptions; + opt.setProcessComments(false); + + opt.setMaxAliasesForCollections(100); // 100 aliases + opt.setCodePointLimit(5 * 1024 * 1024); // 5MB + return opt; }); + /** + * The {@link DumperOptions} for SnakeYAML. + * + * @see DumperOptions + */ ConfigurationOption DUMPER = ConfigurationOption.of(() -> { - DumperOptions options = new DumperOptions(); - options.setIndent(2); - options.setWidth(120); - options.setProcessComments(true); - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - return options; + DumperOptions opt = new DumperOptions(); + + // As we handle comments ourselves, + // we don't want SnakeYAML to read them when saving the configs. + opt.setProcessComments(false); + + opt.setIndent(2); + opt.setWidth(120); + opt.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return opt; }); diff --git a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java index 054062d..d741243 100644 --- a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java +++ b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java @@ -1,29 +1,33 @@ package cc.carm.lib.configuration.source.yaml; -import cc.carm.lib.configuration.commentable.CommentableMetaTypes; +import cc.carm.lib.configuration.commentable.CommentableMeta; +import cc.carm.lib.configuration.option.CommentableOptions; import cc.carm.lib.configuration.source.ConfigurationHolder; import cc.carm.lib.configuration.source.file.FileConfigSource; -import cc.carm.lib.configuration.source.meta.ConfigurationMetadata; +import cc.carm.lib.configuration.source.option.StandardOptions; import cc.carm.lib.configuration.source.section.ConfigureSection; import cc.carm.lib.configuration.source.section.MemorySection; +import cc.carm.lib.yamlcommentupdater.CommentedSection; +import cc.carm.lib.yamlcommentupdater.CommentedYAMLWriter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.comments.CommentLine; -import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.*; import org.yaml.snakeyaml.reader.UnicodeReader; import org.yaml.snakeyaml.representer.Representer; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.Reader; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.stream.Collectors; +import java.util.regex.Pattern; -public class YAMLSource extends FileConfigSource, YAMLSource> { +public class YAMLSource extends FileConfigSource, YAMLSource> implements CommentedSection { protected final @NotNull YamlConstructor yamlConstructor; protected final @NotNull YamlRepresenter yamlRepresenter; @@ -65,6 +69,11 @@ public class YAMLSource extends FileConfigSource, YAMLS return Objects.requireNonNull(this.rootSection, "Root section is not initialized."); } + @Override + public char separator() { + return holder().options().get(StandardOptions.PATH_SEPARATOR); + } + public @NotNull LoaderOptions loaderOptions() { return holder().options().get(YAMLOptions.LOADER); } @@ -77,54 +86,23 @@ public class YAMLSource extends FileConfigSource, YAMLS private MappingNode toNodeTree(@NotNull final ConfigureSection section) { List nodeTuples = new ArrayList<>(); for (final Map.Entry entry : section.getValues(false).entrySet()) { - - final Node keyNode = this.yaml.represent(entry.getKey()); - final Node valueNode; + Node keyNode = this.yaml.represent(entry.getKey()); + Node valueNode; if (entry.getValue() instanceof ConfigureSection) { valueNode = this.toNodeTree((ConfigureSection) entry.getValue()); } else { valueNode = this.yaml.represent(entry.getValue()); } - - keyNode.setBlockComments(buildComments(CommentType.BLOCK, CommentableMetaTypes.HEADER_COMMENTS, entry.getKey())); - if (valueNode instanceof MappingNode || valueNode instanceof SequenceNode) { - keyNode.setInLineComments(buildComment(CommentType.IN_LINE, CommentableMetaTypes.INLINE_COMMENT, entry.getKey())); - } else { - valueNode.setInLineComments(buildComment(CommentType.IN_LINE, CommentableMetaTypes.INLINE_COMMENT, entry.getKey())); - } -// keyNode.setEndComments(buildComments(CommentType.BLOCK, CommentableMetaTypes.FOOTER_COMMENTS, entry.getKey())); - nodeTuples.add(new NodeTuple(keyNode, valueNode)); } return new MappingNode(Tag.MAP, nodeTuples, DumperOptions.FlowStyle.BLOCK); } - public List buildComments(@NotNull CommentType type, @NotNull ConfigurationMetadata> meta, - @Nullable String path) { - List comments = holder.metadata(path).get(meta); - if (comments == null) return Collections.emptyList(); - return comments.stream().map(s -> { - if (s.isEmpty()) return new CommentLine(null, null, "", CommentType.BLANK_LINE); - else return new CommentLine(null, null, s, type); - }).collect(Collectors.toList()); - } - - public List buildComment(@NotNull CommentType type, @NotNull ConfigurationMetadata meta, - @Nullable String path) { - String comment = holder.metadata(path).get(meta); - if (comment == null || comment.isEmpty()) return Collections.emptyList(); - return Collections.singletonList(new CommentLine(null, null, comment, type)); - } - @NotNull - public String saveToString() { - - MappingNode mappingNode = this.toNodeTree(this); -// mappingNode.setBlockComments(this.getCommentLines(this.saveHeader(this.getOptions().getHeader()), CommentType.BLOCK)); -// mappingNode.setEndComments(this.getCommentLines(this.getOptions().getFooter(), CommentType.BLOCK)); - + public String saveToString(ConfigureSection section) { + MappingNode mappingNode = this.toNodeTree(section); StringWriter writer = new StringWriter(); if ((mappingNode.getBlockComments() == null || mappingNode.getBlockComments().isEmpty()) && (mappingNode.getEndComments() == null || mappingNode.getEndComments().isEmpty()) @@ -137,13 +115,21 @@ public class YAMLSource extends FileConfigSource, YAMLS } this.yaml.serialize(mappingNode, writer); } - return writer.toString(); } @Override public void save() throws Exception { - fileWriter(w -> w.write(saveToString())); + CommentedYAMLWriter writer = new CommentedYAMLWriter( + String.valueOf(this.separator()), + dumperOptions().getIndent(), + holder.options().get(CommentableOptions.COMMENT_EMPTY_VALUE) + ); + try { + fileWriter(w -> w.write(writer.saveToString(this))); + } catch (Exception ex) { + fileWriter(w -> w.write(saveToString(section()))); + } } @Override @@ -151,6 +137,11 @@ public class YAMLSource extends FileConfigSource, YAMLS this.rootSection = fileReadString(this::loadFromString); } + @Override + public String toString() { + return this.saveToString(section()); + } + public @NotNull MemorySection loadFromString(@NotNull String data) throws Exception { MappingNode mappingNode; try (Reader reader = new UnicodeReader(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)))) { @@ -186,6 +177,70 @@ public class YAMLSource extends FileConfigSource, YAMLS } } + @Override + public String serializeValue(@NotNull String key, @NotNull Object value) { + Map map = new LinkedHashMap<>(); + map.put(key, value); + return saveToString(MemorySection.root(this, map)); + } + + @Override + public @Nullable Set getKeys(@Nullable String sectionKey, boolean deep) { + if (sectionKey == null) return section().getKeys(deep); + ConfigureSection sub = section().getSection(sectionKey); + if (sub == null) return null; + return sub.getKeys(deep); + } + + @Override + public @Nullable Object getValue(@NotNull String key) { + return get(key); + } + + @Override + public @Nullable String getInlineComment(@NotNull String key) { + String comment = getInlineComment(key, null); + if (comment != null) return comment; + + String sep = String.valueOf(separator()); + + // If the comment is not found, try to get the comment from the parent section + String[] keys = key.split(sep); + if (keys.length == 1) return null; + + // Try every possible parent key&child key combination + for (int i = 1; i < keys.length; i++) { + String parentKey = String.join(sep, Arrays.copyOfRange(keys, 0, i)); + String childKey = String.join(sep, Arrays.copyOfRange(keys, i, keys.length)); + comment = getInlineComment(childKey, parentKey); + if (comment != null) return comment; + } + return null; + } + + public @Nullable String getInlineComment(@NotNull String key, @Nullable String sectionKey) { + Map pathComment = holder().metadata(key).get(CommentableMeta.INLINE); + if (pathComment == null || pathComment.isEmpty()) return null; + if (sectionKey == null) return pathComment.get(null); + + for (Map.Entry entry : pathComment.entrySet()) { + if (entry.getKey().equals(sectionKey)) return entry.getValue(); + Pattern pattern = Pattern.compile(entry.getKey().replace(".", "\\.").replace("*", ".*")); + if (pattern.matcher(sectionKey).matches()) return entry.getValue(); + } + return null; + } + + @Override + public @Nullable List getHeaderComments(@Nullable String key) { + return holder().metadata(key).get(CommentableMeta.HEADER); + } + + @Override + public @Nullable List getFooterComments(@Nullable String key) { + return holder().metadata(key).get(CommentableMeta.FOOTER); + } + public static class YamlRepresenter extends Representer { public YamlRepresenter(@NotNull final DumperOptions dumperOptions) { @@ -213,7 +268,7 @@ public class YAMLSource extends FileConfigSource, YAMLS } @Override - protected void flattenMapping(@NotNull final MappingNode mappingNode) { + public void flattenMapping(@NotNull final MappingNode mappingNode) { super.flattenMapping(mappingNode); } } diff --git a/providers/yaml/src/test/java/sample/SampleConfig.java b/providers/yaml/src/test/java/sample/SampleConfig.java index 01720f0..3cb9254 100644 --- a/providers/yaml/src/test/java/sample/SampleConfig.java +++ b/providers/yaml/src/test/java/sample/SampleConfig.java @@ -2,7 +2,7 @@ package sample; import cc.carm.lib.configuration.Configuration; import cc.carm.lib.configuration.annotation.ConfigPath; -import cc.carm.lib.configuration.annotation.HeaderComment; +import cc.carm.lib.configuration.annotation.HeaderComments; import cc.carm.lib.configuration.annotation.InlineComment; import cc.carm.lib.configuration.value.standard.ConfiguredList; import cc.carm.lib.configuration.value.standard.ConfiguredValue; @@ -10,12 +10,15 @@ import cc.carm.lib.configuration.value.standard.ConfiguredValue; import java.util.UUID; @ConfigPath(root = true) -@HeaderComment("Configurations for sample") +@HeaderComments("Configurations for sample") public interface SampleConfig extends Configuration { @InlineComment("Enabled?") // Inline comment ConfiguredValue ENABLED = ConfiguredValue.of(true); + @HeaderComments("Server configurations") // Header comment + ConfiguredValue PORT = ConfiguredValue.of(Integer.class); + ConfiguredList UUIDS = ConfiguredList.builderOf(UUID.class).fromString() .parse(UUID::fromString).serialize(UUID::toString) .defaults( @@ -26,11 +29,12 @@ public interface SampleConfig extends Configuration { @ConfigPath("info") // Custom path interface INFO extends Configuration { - @HeaderComment("Configure your name!") // Header comment + @HeaderComments("Configure your name!") // Header comment ConfiguredValue NAME = ConfiguredValue.of("Joker"); - @ConfigPath("year") // Custom path + @ConfigPath("how-old-are-you") // Custom path ConfiguredValue AGE = ConfiguredValue.of(24); } + } diff --git a/providers/yaml/src/test/java/sample/SampleTest.java b/providers/yaml/src/test/java/sample/SampleTest.java index c80c4d0..dbf4ffa 100644 --- a/providers/yaml/src/test/java/sample/SampleTest.java +++ b/providers/yaml/src/test/java/sample/SampleTest.java @@ -17,7 +17,11 @@ public class SampleTest { // 2. Initialize the configuration classes or instances. holder.initialize(SampleConfig.class); // 3. Enjoy using the configuration! + System.out.println("Enabled? -> " + SampleConfig.ENABLED.resolve()); SampleConfig.ENABLED.set(false); + System.out.println("And now? -> " + SampleConfig.ENABLED.resolve()); + // p.s. Changes not save so enable value will still be true in the next run. + System.out.println("Your name is " + SampleConfig.INFO.NAME.resolve() + " (age=" + SampleConfig.INFO.AGE.resolve() + ")!"); } diff --git a/providers/yaml/src/test/java/yaml/test/YamlTests.java b/providers/yaml/src/test/java/yaml/test/YamlTests.java new file mode 100644 index 0000000..0d1e892 --- /dev/null +++ b/providers/yaml/src/test/java/yaml/test/YamlTests.java @@ -0,0 +1,45 @@ +package yaml.test; + +import cc.carm.lib.configuration.commentable.CommentableMeta; +import cc.carm.lib.configuration.demo.tests.ConfigurationTest; +import cc.carm.lib.configuration.source.ConfigurationHolder; +import cc.carm.lib.configuration.source.yaml.YAMLConfigFactory; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +public class YamlTests { + + @Test + public void test() { + + ConfigurationHolder holder = YAMLConfigFactory.from("target/tests.yml") + .resourcePath("configs/sample.yml").build(); + + ConfigurationTest.testDemo(holder); + ConfigurationTest.testInner(holder); + + + Map> headers = holder.extractMetadata(CommentableMeta.HEADER); + System.out.println("Header comments: "); + headers.forEach((k, v) -> { + if (v.isEmpty()) return; + System.out.println("- " + k + ": "); + v.forEach(s -> System.out.println("- | " + s)); + }); + + Map> footers = holder.extractMetadata(CommentableMeta.FOOTER); + System.out.println("Footer comments: "); + footers.forEach((k, v) -> { + if (v.isEmpty()) return; + System.out.println("- " + k + ": "); + v.forEach(s -> System.out.println("- | " + s)); + }); + + + ConfigurationTest.save(holder); + } + +} +