mirror of
https://github.com/CarmJos/EasyConfiguration.git
synced 2026-06-04 18:48:20 +08:00
feat: Optimized comments & sections behavior
This commit is contained in:
@@ -10,12 +10,6 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
public class PrimitiveAdapter<T> extends ValueAdapter<T> {
|
public class PrimitiveAdapter<T> extends ValueAdapter<T> {
|
||||||
|
|
||||||
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[]{
|
public static final String[] TRUE_VALUES = new String[]{
|
||||||
"true", "yes", "on", "1", "enabled", "enable", "active"
|
"true", "yes", "on", "1", "enabled", "enable", "active"
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-1
@@ -3,10 +3,21 @@ package cc.carm.lib.configuration.adapter.strandard;
|
|||||||
import cc.carm.lib.configuration.adapter.ValueAdapter;
|
import cc.carm.lib.configuration.adapter.ValueAdapter;
|
||||||
import cc.carm.lib.configuration.adapter.ValueType;
|
import cc.carm.lib.configuration.adapter.ValueType;
|
||||||
import cc.carm.lib.configuration.source.section.ConfigureSection;
|
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 {
|
public interface StandardAdapters {
|
||||||
|
|
||||||
ValueAdapter<ConfigureSection> 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<Enum<?>> ENUMS = PrimitiveAdapter.ofEnum();
|
||||||
|
|
||||||
|
@NotNull ValueAdapter<ConfigureSection> SECTIONS = new ValueAdapter<>(
|
||||||
ValueType.of(ConfigureSection.class),
|
ValueType.of(ConfigureSection.class),
|
||||||
(provider, type, value) -> value,
|
(provider, type, value) -> value,
|
||||||
(provider, type, value) -> {
|
(provider, type, value) -> {
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package cc.carm.lib.configuration.source;
|
package cc.carm.lib.configuration.source;
|
||||||
|
|
||||||
import cc.carm.lib.configuration.adapter.*;
|
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.adapter.strandard.StandardAdapters;
|
||||||
import cc.carm.lib.configuration.function.DataFunction;
|
import cc.carm.lib.configuration.function.DataFunction;
|
||||||
import cc.carm.lib.configuration.source.loader.ConfigurationInitializer;
|
import cc.carm.lib.configuration.source.loader.ConfigurationInitializer;
|
||||||
import cc.carm.lib.configuration.source.loader.PathGenerator;
|
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.meta.ConfigurationMetadata;
|
||||||
import cc.carm.lib.configuration.source.option.ConfigurationOption;
|
import cc.carm.lib.configuration.source.option.ConfigurationOption;
|
||||||
import cc.carm.lib.configuration.source.option.ConfigurationOptionHolder;
|
import cc.carm.lib.configuration.source.option.ConfigurationOptionHolder;
|
||||||
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@@ -25,11 +28,13 @@ public abstract class ConfigurationFactory<
|
|||||||
|
|
||||||
protected ValueAdapterRegistry adapters = new ValueAdapterRegistry();
|
protected ValueAdapterRegistry adapters = new ValueAdapterRegistry();
|
||||||
protected ConfigurationOptionHolder options = new ConfigurationOptionHolder();
|
protected ConfigurationOptionHolder options = new ConfigurationOptionHolder();
|
||||||
|
protected @NotNull Map<String, ConfigurationMetaHolder> metadata = new HashMap<>();
|
||||||
protected ConfigurationInitializer initializer = new ConfigurationInitializer();
|
protected ConfigurationInitializer initializer = new ConfigurationInitializer();
|
||||||
|
|
||||||
public ConfigurationFactory() {
|
public ConfigurationFactory() {
|
||||||
this.adapters.register(PrimitiveAdapter.ADAPTERS);
|
this.adapters.register(StandardAdapters.PRIMITIVES);
|
||||||
this.adapters.register(StandardAdapters.SECTION_ADAPTER);
|
this.adapters.register(StandardAdapters.SECTIONS);
|
||||||
|
this.adapters.register(StandardAdapters.ENUMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract SELF self();
|
protected abstract SELF self();
|
||||||
@@ -102,6 +107,27 @@ public abstract class ConfigurationFactory<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SELF metadata(@NotNull Map<String, ConfigurationMetaHolder> metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SELF metadata(@NotNull Consumer<Map<String, ConfigurationMetaHolder>> 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<ConfigurationMetaHolder> handler) {
|
||||||
|
return metadata(map -> {
|
||||||
|
ConfigurationMetaHolder meta = map.computeIfAbsent(path, k -> new ConfigurationMetaHolder());
|
||||||
|
handler.accept(meta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public SELF initializer(ConfigurationInitializer initializer) {
|
public SELF initializer(ConfigurationInitializer initializer) {
|
||||||
this.initializer = initializer;
|
this.initializer = initializer;
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ import cc.carm.lib.configuration.adapter.ValueAdapterRegistry;
|
|||||||
import cc.carm.lib.configuration.adapter.ValueType;
|
import cc.carm.lib.configuration.adapter.ValueType;
|
||||||
import cc.carm.lib.configuration.source.loader.ConfigurationInitializer;
|
import cc.carm.lib.configuration.source.loader.ConfigurationInitializer;
|
||||||
import cc.carm.lib.configuration.source.meta.ConfigurationMetaHolder;
|
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.option.ConfigurationOptionHolder;
|
||||||
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
||||||
import cc.carm.lib.configuration.value.ValueManifest;
|
import cc.carm.lib.configuration.value.ValueManifest;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.UnmodifiableView;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public abstract class ConfigurationHolder<SOURCE extends ConfigureSource<?, ?, SOURCE>> {
|
public abstract class ConfigurationHolder<SOURCE extends ConfigureSource<?, ?, SOURCE>> {
|
||||||
@@ -54,6 +58,16 @@ public abstract class ConfigurationHolder<SOURCE extends ConfigureSource<?, ?, S
|
|||||||
return metadata().computeIfAbsent(path, k -> new ConfigurationMetaHolder());
|
return metadata().computeIfAbsent(path, k -> new ConfigurationMetaHolder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@UnmodifiableView
|
||||||
|
public <M> Map<String, M> extractMetadata(@NotNull ConfigurationMetadata<M> type) {
|
||||||
|
Map<String, M> metas = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, ConfigurationMetaHolder> 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() {
|
public ValueAdapterRegistry adapters() {
|
||||||
return this.adapters;
|
return this.adapters;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public abstract class ConfigValue<T> extends ValueManifest<T> {
|
|||||||
*
|
*
|
||||||
* @return Non-null value
|
* @return Non-null value
|
||||||
* @throws NullPointerException Thrown when the corresponding data is null
|
* @throws NullPointerException Thrown when the corresponding data is null
|
||||||
* @see #resolve()
|
* @see #resolve() for a more descriptive function
|
||||||
*/
|
*/
|
||||||
public @NotNull T getNotNull() {
|
public @NotNull T getNotNull() {
|
||||||
return resolve();
|
return resolve();
|
||||||
@@ -114,7 +114,7 @@ public abstract class ConfigValue<T> extends ValueManifest<T> {
|
|||||||
*/
|
*/
|
||||||
public void setDefault(boolean override) {
|
public void setDefault(boolean override) {
|
||||||
if (!override && config().contains(path())) return;
|
if (!override && config().contains(path())) return;
|
||||||
Optional.ofNullable(defaults()).ifPresent(this::set);
|
set(defaults()); // Set the default value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ package cc.carm.lib.configuration.demo;
|
|||||||
|
|
||||||
import cc.carm.lib.configuration.Configuration;
|
import cc.carm.lib.configuration.Configuration;
|
||||||
import cc.carm.lib.configuration.annotation.ConfigPath;
|
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.ConfigValue;
|
||||||
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
||||||
|
|
||||||
@HeaderComment({"", "数据库配置", " 用于提供数据库连接,进行数据库操作。"})
|
@HeaderComments({"", "数据库配置", " 用于提供数据库连接,进行数据库操作。"})
|
||||||
public class DatabaseConfiguration implements Configuration {
|
public class DatabaseConfiguration implements Configuration {
|
||||||
|
|
||||||
@ConfigPath("driver")
|
@ConfigPath("driver")
|
||||||
@HeaderComment({
|
@HeaderComments({
|
||||||
"数据库驱动配置,请根据数据库类型设置。",
|
"数据库驱动配置,请根据数据库类型设置。",
|
||||||
"- MySQL(旧): com.mysql.jdbc.Driver",
|
"- MySQL(旧): com.mysql.jdbc.Driver",
|
||||||
"- MySQL(新): com.mysql.cj.jdbc.Driver",
|
"- MySQL(新): com.mysql.cj.jdbc.Driver",
|
||||||
|
|||||||
+15
-8
@@ -2,7 +2,8 @@ package cc.carm.lib.configuration.demo.tests.conf;
|
|||||||
|
|
||||||
import cc.carm.lib.configuration.Configuration;
|
import cc.carm.lib.configuration.Configuration;
|
||||||
import cc.carm.lib.configuration.annotation.ConfigPath;
|
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.annotation.InlineComment;
|
||||||
import cc.carm.lib.configuration.demo.DatabaseConfiguration;
|
import cc.carm.lib.configuration.demo.DatabaseConfiguration;
|
||||||
import cc.carm.lib.configuration.demo.tests.model.UserRecord;
|
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.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ConfigPath(root = true)
|
||||||
@HeaderComment({"此处内容将显示在配置文件的最上方"})
|
@HeaderComments({"此处内容将显示在配置文件的最上方"})
|
||||||
|
@FooterComments({"此处内容将显示在配置文件的最下方", "可用于显示版权信息等"})
|
||||||
public interface DemoConfiguration extends Configuration {
|
public interface DemoConfiguration extends Configuration {
|
||||||
|
|
||||||
@ConfigPath(root = true)
|
@ConfigPath(root = true)
|
||||||
ConfigValue<Double> VERSION = ConfiguredValue.of(Double.class, 1.0D);
|
ConfigValue<Double> VERSION = ConfiguredValue.of(Double.class, 1.0D);
|
||||||
|
|
||||||
@ConfigPath(root = true)
|
@ConfigPath(root = true)
|
||||||
|
@FooterComments({"此处内容将显示在配置条目的下方", "可用于补充说明,但一般不建议使用"})
|
||||||
ConfigValue<Long> TEST_NUMBER = ConfiguredValue.of(1000000L);
|
ConfigValue<Long> TEST_NUMBER = ConfiguredValue.of(1000000L);
|
||||||
|
|
||||||
|
@HeaderComments({"枚举类型测试"})
|
||||||
|
@FooterComments({"上述的枚举内容本质上是通过STRING解析的"})
|
||||||
ConfigValue<ChronoUnit> TEST_ENUM = ConfiguredValue.of(ChronoUnit.class, ChronoUnit.DAYS);
|
ConfigValue<ChronoUnit> TEST_ENUM = ConfiguredValue.of(ChronoUnit.class, ChronoUnit.DAYS);
|
||||||
|
|
||||||
// 支持通过 Class<?> 变量标注子配置,一并注册。
|
// 支持通过 Class<?> 变量标注子配置,一并注册。
|
||||||
@@ -31,8 +36,10 @@ public interface DemoConfiguration extends Configuration {
|
|||||||
Class<?> DATABASE = DatabaseConfiguration.class;
|
Class<?> DATABASE = DatabaseConfiguration.class;
|
||||||
|
|
||||||
@ConfigPath("registered_users") // 通过注解规定配置文件中的路径,若不进行注解则以变量名自动生成。
|
@ConfigPath("registered_users") // 通过注解规定配置文件中的路径,若不进行注解则以变量名自动生成。
|
||||||
@HeaderComment({"Section类型数据测试"}) // 通过注解给配置添加注释。
|
@HeaderComments({"Section类型数据测试"}) // 通过注解给配置添加注释。
|
||||||
@InlineComment("Section数据也支持InlineComment注释")
|
@InlineComment("默认地注释会加到Section的首行末尾") // 通过注解给配置添加注释。
|
||||||
|
@InlineComment(value = "用户名(匹配注释)", regex = "name") // 通过注解给配置添加注释。
|
||||||
|
@InlineComment(value = "信息", regex = "info.*") // 通过注解给配置添加注释。
|
||||||
ConfiguredList<UserRecord> USERS = ConfiguredList.builderOf(UserRecord.class).fromSection()
|
ConfiguredList<UserRecord> USERS = ConfiguredList.builderOf(UserRecord.class).fromSection()
|
||||||
.parse(UserRecord::deserialize).serialize(UserRecord::serialize)
|
.parse(UserRecord::deserialize).serialize(UserRecord::serialize)
|
||||||
.defaults(UserRecord.CARM).build();
|
.defaults(UserRecord.CARM).build();
|
||||||
@@ -52,15 +59,15 @@ public interface DemoConfiguration extends Configuration {
|
|||||||
class SUB implements Configuration {
|
class SUB implements Configuration {
|
||||||
|
|
||||||
@ConfigPath(value = "uuid-value", root = true)
|
@ConfigPath(value = "uuid-value", root = true)
|
||||||
@InlineComment("This is an inline comment")
|
|
||||||
public static final ConfigValue<UUID> UUID_CONFIG_VALUE = ConfiguredValue
|
public static final ConfigValue<UUID> UUID_CONFIG_VALUE = ConfiguredValue
|
||||||
.builderOf(UUID.class).fromString()
|
.builderOf(UUID.class).fromString()
|
||||||
.parse((holder, data) -> UUID.fromString(data))
|
.parse((holder, data) -> UUID.fromString(data))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static class That implements Configuration {
|
@HeaderComments({"内部类的内部类测试", "通过这种方式,您可以轻易实现多层次的配置文件结构"})
|
||||||
|
public interface That extends Configuration {
|
||||||
|
|
||||||
public static final ConfiguredList<UUID> OPERATORS = ConfiguredList
|
ConfiguredList<UUID> OPERATORS = ConfiguredList
|
||||||
.builderOf(UUID.class).fromString()
|
.builderOf(UUID.class).fromString()
|
||||||
.parse(s -> Objects.requireNonNull(UUID.fromString(s)))
|
.parse(s -> Objects.requireNonNull(UUID.fromString(s)))
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package cc.carm.lib.configuration.demo.tests.conf;
|
package cc.carm.lib.configuration.demo.tests.conf;
|
||||||
|
|
||||||
import cc.carm.lib.configuration.Configuration;
|
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.ConfigValue;
|
||||||
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
||||||
|
|
||||||
@HeaderComment("Inner Test")
|
@HeaderComments("Inner Test")
|
||||||
public class InstanceConfig implements Configuration {
|
public class InstanceConfig implements Configuration {
|
||||||
|
|
||||||
public final ConfigValue<Double> INNER_VALUE = ConfiguredValue.of(1.0D);
|
public final ConfigValue<Double> INNER_VALUE = ConfiguredValue.of(1.0D);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package cc.carm.lib.configuration.demo.tests.conf;
|
|||||||
|
|
||||||
import cc.carm.lib.configuration.Configuration;
|
import cc.carm.lib.configuration.Configuration;
|
||||||
import cc.carm.lib.configuration.annotation.ConfigPath;
|
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.annotation.InlineComment;
|
||||||
import cc.carm.lib.configuration.demo.tests.model.UserRecord;
|
import cc.carm.lib.configuration.demo.tests.model.UserRecord;
|
||||||
import cc.carm.lib.configuration.value.ConfigValue;
|
import cc.carm.lib.configuration.value.ConfigValue;
|
||||||
@@ -12,12 +13,11 @@ import java.util.UUID;
|
|||||||
|
|
||||||
public class RegistryConfig implements Configuration {
|
public class RegistryConfig implements Configuration {
|
||||||
|
|
||||||
@HeaderComment("Support for configurations as instances")
|
@HeaderComments("Support for configurations as instances")
|
||||||
public final InstanceConfig INSTANCE = new InstanceConfig();
|
public final InstanceConfig INSTANCE = new InstanceConfig();
|
||||||
|
|
||||||
@ConfigPath("test.user") // 通过注解规定配置文件中的路径,若不进行注解则以变量名自动生成。
|
@ConfigPath("test.user") // 通过注解规定配置文件中的路径,若不进行注解则以变量名自动生成。
|
||||||
@HeaderComment({"Section类型数据测试"}) // 通过注解给配置添加注释。
|
@FooterComments({"12313213212"})
|
||||||
@InlineComment("Section数据也支持InlineComment注释")
|
|
||||||
public final ConfigValue<UserRecord> TEST_MODEL = ConfiguredValue.builderOf(UserRecord.class).fromSection()
|
public final ConfigValue<UserRecord> TEST_MODEL = ConfiguredValue.builderOf(UserRecord.class).fromSection()
|
||||||
.defaults(new UserRecord("Carm", UUID.randomUUID()))
|
.defaults(new UserRecord("Carm", UUID.randomUUID()))
|
||||||
.parse((holder, section) -> UserRecord.deserialize(section))
|
.parse((holder, section) -> UserRecord.deserialize(section))
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ import java.lang.annotation.Target;
|
|||||||
*/
|
*/
|
||||||
@Target({ElementType.TYPE, ElementType.FIELD})
|
@Target({ElementType.TYPE, ElementType.FIELD})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@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.
|
* If the content of the note is 0, it will be treated as a blank line.
|
||||||
+1
-1
@@ -19,7 +19,7 @@ import java.lang.annotation.Target;
|
|||||||
*/
|
*/
|
||||||
@Target({ElementType.TYPE, ElementType.FIELD})
|
@Target({ElementType.TYPE, ElementType.FIELD})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@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.
|
* If the content of the note is 0, it will be treated as a blank line.
|
||||||
+4
-5
@@ -2,10 +2,7 @@ package cc.carm.lib.configuration.annotation;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.*;
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline comments,
|
* Inline comments,
|
||||||
@@ -17,6 +14,7 @@ import java.lang.annotation.Target;
|
|||||||
*/
|
*/
|
||||||
@Target({ElementType.FIELD})
|
@Target({ElementType.FIELD})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Repeatable(InlineComments.class)
|
||||||
public @interface InlineComment {
|
public @interface InlineComment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,8 +33,9 @@ public @interface InlineComment {
|
|||||||
* If the regex is not empty, the comment will be added to
|
* If the regex is not empty, the comment will be added to
|
||||||
* all sub paths if the regex matches the value.
|
* all sub paths if the regex matches the value.
|
||||||
* If the regex is empty, the comment will be added to the current path.
|
* If the regex is empty, the comment will be added to the current path.
|
||||||
* <p> e.g. <b>"^foo\\.*\\.bar"</b> will be set like
|
* <p> e.g. for section, set <b>{"^foo", "*", "bar"}</b> will be set like
|
||||||
* <blockquote><pre>
|
* <blockquote><pre>
|
||||||
|
* section:
|
||||||
* foo:
|
* foo:
|
||||||
* some:
|
* some:
|
||||||
* lover: "bar" <- not matched so no comments
|
* lover: "bar" <- not matched so no comments
|
||||||
|
|||||||
+15
@@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
+67
@@ -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<List<String>> HEADER = ConfigurationMetadata.of(Collections.emptyList());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration's footer comments
|
||||||
|
*
|
||||||
|
* @see FooterComments
|
||||||
|
*/
|
||||||
|
ConfigurationMetadata<List<String>> FOOTER = ConfigurationMetadata.of(Collections.emptyList());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration's {@link InlineComment}
|
||||||
|
* <p> Map< regex, comment > , regex is used to match the key, null for current path.
|
||||||
|
*
|
||||||
|
* @see InlineComment
|
||||||
|
*/
|
||||||
|
ConfigurationMetadata<Map<String, String>> 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<String, String> 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
-49
@@ -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<List<String>> HEADER_COMMENTS = ConfigurationMetadata.of(Collections.emptyList());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration's footer comments
|
|
||||||
*/
|
|
||||||
ConfigurationMetadata<List<String>> FOOTER_COMMENTS = ConfigurationMetadata.of(Collections.emptyList());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration's {@link InlineComment}
|
|
||||||
*/
|
|
||||||
ConfigurationMetadata<String> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+2
-2
@@ -8,7 +8,7 @@ public interface CommentableOptions {
|
|||||||
// * Whether to keep modified comments in configuration,
|
// * Whether to keep modified comments in configuration,
|
||||||
// * that means we only set comments for values that are not exists in configuration.
|
// * that means we only set comments for values that are not exists in configuration.
|
||||||
// */
|
// */
|
||||||
// ConfigurationOption<Boolean> KEEP_COMMENTS = ConfigurationOption.of(true);
|
// ConfigurationOption<Boolean> 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.
|
* Whether to comment values name that are not exists in configuration and no default value offered.
|
||||||
@@ -19,6 +19,6 @@ public interface CommentableOptions {
|
|||||||
* # foo:
|
* # foo:
|
||||||
* </pre></blockquote>
|
* </pre></blockquote>
|
||||||
*/
|
*/
|
||||||
ConfigurationOption<Boolean> COMMENT_EMPTY_VALUE = ConfigurationOption.of(true);
|
ConfigurationOption<Boolean> COMMENT_EMPTY_VALUE = ConfigurationOption.of(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-18
@@ -68,11 +68,7 @@ public class MemorySection implements ConfigureSection {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Map<String, Object> getValues(boolean deep) {
|
public @NotNull Map<String, Object> getValues(boolean deep) {
|
||||||
if (deep) {
|
return Collections.unmodifiableMap(deep ? mapChildrenValues(this, null, true) : data());
|
||||||
Map<String, Object> values = new LinkedHashMap<>();
|
|
||||||
mapChildrenValues(values, this, null, true);
|
|
||||||
return Collections.unmodifiableMap(values);
|
|
||||||
} else return Collections.unmodifiableMap(data());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -81,13 +77,11 @@ public class MemorySection implements ConfigureSection {
|
|||||||
|
|
||||||
MemorySection section = getSectionFor(path);
|
MemorySection section = getSectionFor(path);
|
||||||
if (section == this) {
|
if (section == this) {
|
||||||
if (value == null) {
|
// Even this value is null, we still need to put it in the map
|
||||||
this.data.remove(path);
|
// to ensure that the path is marked as existing.
|
||||||
} else {
|
this.data.put(path, value);
|
||||||
this.data.put(path, value);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
section.set(getChild(path), value);
|
section.set(childPath(path), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +93,7 @@ public class MemorySection implements ConfigureSection {
|
|||||||
@Override
|
@Override
|
||||||
public @Nullable Object get(@NotNull String path) {
|
public @Nullable Object get(@NotNull String path) {
|
||||||
MemorySection section = getSectionFor(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
|
@Override
|
||||||
@@ -138,29 +132,29 @@ public class MemorySection implements ConfigureSection {
|
|||||||
return (MemorySection) section;
|
return (MemorySection) section;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getChild(String path) {
|
private String childPath(String path) {
|
||||||
int index = path.indexOf(separator());
|
int index = path.indexOf(separator());
|
||||||
return (index == -1) ? path : path.substring(index + 1);
|
return (index == -1) ? path : path.substring(index + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map the values of the children of the section to the output map.
|
* 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 section The section to map the values from
|
||||||
* @param parent The parent path
|
* @param parent The parent path
|
||||||
* @param deep If the mapping should be deep
|
* @param deep If the mapping should be deep
|
||||||
*/
|
*/
|
||||||
protected void mapChildrenValues(@NotNull Map<String, Object> output, @NotNull MemorySection section,
|
protected Map<String, Object> mapChildrenValues(@NotNull MemorySection section,
|
||||||
@Nullable String parent, boolean deep) {
|
@Nullable String parent, boolean deep) {
|
||||||
|
Map<String, Object> output = new LinkedHashMap<>();
|
||||||
for (Map.Entry<String, Object> entry : section.data().entrySet()) {
|
for (Map.Entry<String, Object> entry : section.data().entrySet()) {
|
||||||
String path = (parent == null ? "" : parent + separator()) + entry.getKey();
|
String path = (parent == null ? "" : parent + separator()) + entry.getKey();
|
||||||
output.remove(path);
|
output.remove(path);
|
||||||
output.put(path, entry.getValue());
|
output.put(path, entry.getValue());
|
||||||
if (deep && entry.getValue() instanceof MemorySection) {
|
if (deep && entry.getValue() instanceof MemorySection) {
|
||||||
this.mapChildrenValues(output, (MemorySection) entry.getValue(), path, true);
|
output.putAll(mapChildrenValues((MemorySection) entry.getValue(), path, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -7,8 +7,6 @@ import com.google.gson.GsonBuilder;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ public class JSONConfigFactory extends FileConfigFactory<JSONSource, Configurati
|
|||||||
File configFile = this.file;
|
File configFile = this.file;
|
||||||
String sourcePath = this.resourcePath;
|
String sourcePath = this.resourcePath;
|
||||||
|
|
||||||
return new ConfigurationHolder<JSONSource>(this.adapters, this.options, new HashMap<>(), this.initializer) {
|
return new ConfigurationHolder<JSONSource>(this.adapters, this.options, this.metadata, this.initializer) {
|
||||||
final JSONSource source = new JSONSource(this, configFile, sourcePath, gson);
|
final JSONSource source = new JSONSource(this, configFile, sourcePath, gson);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -67,15 +67,15 @@ public class JSONSource extends FileConfigSource<MemorySection, Map<?, ?>, JSONS
|
|||||||
fileWriter(writer -> gson.toJson(original(), writer));
|
fileWriter(writer -> gson.toJson(original(), writer));
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull String saveToString() {
|
@Override
|
||||||
return gson.toJson(original());
|
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
|
@Override
|
||||||
protected void onReload() throws Exception {
|
public String toString() {
|
||||||
this.rootSection = MemorySection.root(
|
return gson.toJson(original());
|
||||||
this, fileReader(reader -> gson.fromJson(reader, LinkedHashMap.class))
|
|
||||||
);
|
|
||||||
this.lastUpdateMillis = System.currentTimeMillis(); // 更新时间
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<maven.compiler.target>${project.jdk.version}</maven.compiler.target>
|
<maven.compiler.target>${project.jdk.version}</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||||
|
<deps.yamlcommentwriter.version>1.1.0</deps.yamlcommentwriter.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<artifactId>easyconfiguration-yaml</artifactId>
|
<artifactId>easyconfiguration-yaml</artifactId>
|
||||||
@@ -56,6 +57,13 @@
|
|||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cc.carm.lib</groupId>
|
||||||
|
<artifactId>yamlcommentwriter</artifactId>
|
||||||
|
<version>${deps.yamlcommentwriter.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.yaml</groupId>
|
<groupId>org.yaml</groupId>
|
||||||
<artifactId>snakeyaml</artifactId>
|
<artifactId>snakeyaml</artifactId>
|
||||||
|
|||||||
+19
-7
@@ -1,6 +1,6 @@
|
|||||||
package cc.carm.lib.configuration.source.yaml;
|
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.ConfigurationHolder;
|
||||||
import cc.carm.lib.configuration.source.file.FileConfigFactory;
|
import cc.carm.lib.configuration.source.file.FileConfigFactory;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -9,8 +9,7 @@ import org.yaml.snakeyaml.DumperOptions;
|
|||||||
import org.yaml.snakeyaml.LoaderOptions;
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.HashMap;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class YAMLConfigFactory extends FileConfigFactory<YAMLSource, ConfigurationHolder<YAMLSource>, YAMLConfigFactory> {
|
public class YAMLConfigFactory extends FileConfigFactory<YAMLSource, ConfigurationHolder<YAMLSource>, YAMLConfigFactory> {
|
||||||
@@ -60,17 +59,30 @@ public class YAMLConfigFactory extends FileConfigFactory<YAMLSource, Configurati
|
|||||||
return option(YAMLOptions.DUMPER, modifier);
|
return option(YAMLOptions.DUMPER, modifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the header comments for the configuration file.
|
||||||
|
* <p> 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
|
@Override
|
||||||
public @NotNull ConfigurationHolder<YAMLSource> build() {
|
public @NotNull ConfigurationHolder<YAMLSource> build() {
|
||||||
|
|
||||||
File configFile = this.file;
|
File configFile = this.file;
|
||||||
String sourcePath = this.resourcePath;
|
String sourcePath = this.resourcePath;
|
||||||
|
|
||||||
CommentableMetaTypes.register(this.initializer); // Register commentable meta types
|
CommentableMeta.register(this.initializer); // Register commentable meta types
|
||||||
|
|
||||||
return new ConfigurationHolder<YAMLSource>(
|
return new ConfigurationHolder<YAMLSource>(this.adapters, this.options, this.metadata, this.initializer) {
|
||||||
this.adapters, this.options, new HashMap<>(), this.initializer
|
|
||||||
) {
|
|
||||||
final @NotNull YAMLSource source = new YAMLSource(this, configFile, sourcePath);
|
final @NotNull YAMLSource source = new YAMLSource(this, configFile, sourcePath);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+27
-12
@@ -6,24 +6,39 @@ import org.yaml.snakeyaml.LoaderOptions;
|
|||||||
|
|
||||||
public interface YAMLOptions {
|
public interface YAMLOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link LoaderOptions} for SnakeYAML.
|
||||||
|
*
|
||||||
|
* @see LoaderOptions
|
||||||
|
*/
|
||||||
ConfigurationOption<LoaderOptions> LOADER = ConfigurationOption.of(() -> {
|
ConfigurationOption<LoaderOptions> LOADER = ConfigurationOption.of(() -> {
|
||||||
LoaderOptions loaderOptions = new LoaderOptions();
|
LoaderOptions opt = new LoaderOptions();
|
||||||
|
|
||||||
// As we handle comments ourselves,
|
// As we handle comments ourselves,
|
||||||
// we don't want SnakeYAML to read them when loading the configs.
|
// we don't want SnakeYAML to read them when loading the configs.
|
||||||
loaderOptions.setProcessComments(false);
|
opt.setProcessComments(false);
|
||||||
loaderOptions.setMaxAliasesForCollections(100); // 100 aliases
|
|
||||||
loaderOptions.setCodePointLimit(5 * 1024 * 1024); // 5MB
|
opt.setMaxAliasesForCollections(100); // 100 aliases
|
||||||
return loaderOptions;
|
opt.setCodePointLimit(5 * 1024 * 1024); // 5MB
|
||||||
|
return opt;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link DumperOptions} for SnakeYAML.
|
||||||
|
*
|
||||||
|
* @see DumperOptions
|
||||||
|
*/
|
||||||
ConfigurationOption<DumperOptions> DUMPER = ConfigurationOption.of(() -> {
|
ConfigurationOption<DumperOptions> DUMPER = ConfigurationOption.of(() -> {
|
||||||
DumperOptions options = new DumperOptions();
|
DumperOptions opt = new DumperOptions();
|
||||||
options.setIndent(2);
|
|
||||||
options.setWidth(120);
|
// As we handle comments ourselves,
|
||||||
options.setProcessComments(true);
|
// we don't want SnakeYAML to read them when saving the configs.
|
||||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
opt.setProcessComments(false);
|
||||||
return options;
|
|
||||||
|
opt.setIndent(2);
|
||||||
|
opt.setWidth(120);
|
||||||
|
opt.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||||
|
return opt;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+100
-45
@@ -1,29 +1,33 @@
|
|||||||
package cc.carm.lib.configuration.source.yaml;
|
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.ConfigurationHolder;
|
||||||
import cc.carm.lib.configuration.source.file.FileConfigSource;
|
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.ConfigureSection;
|
||||||
import cc.carm.lib.configuration.source.section.MemorySection;
|
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.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.yaml.snakeyaml.DumperOptions;
|
import org.yaml.snakeyaml.DumperOptions;
|
||||||
import org.yaml.snakeyaml.LoaderOptions;
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
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.constructor.SafeConstructor;
|
||||||
import org.yaml.snakeyaml.nodes.*;
|
import org.yaml.snakeyaml.nodes.*;
|
||||||
import org.yaml.snakeyaml.reader.UnicodeReader;
|
import org.yaml.snakeyaml.reader.UnicodeReader;
|
||||||
import org.yaml.snakeyaml.representer.Representer;
|
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.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLSource> {
|
public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLSource> implements CommentedSection {
|
||||||
|
|
||||||
protected final @NotNull YamlConstructor yamlConstructor;
|
protected final @NotNull YamlConstructor yamlConstructor;
|
||||||
protected final @NotNull YamlRepresenter yamlRepresenter;
|
protected final @NotNull YamlRepresenter yamlRepresenter;
|
||||||
@@ -65,6 +69,11 @@ public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLS
|
|||||||
return Objects.requireNonNull(this.rootSection, "Root section is not initialized.");
|
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() {
|
public @NotNull LoaderOptions loaderOptions() {
|
||||||
return holder().options().get(YAMLOptions.LOADER);
|
return holder().options().get(YAMLOptions.LOADER);
|
||||||
}
|
}
|
||||||
@@ -77,54 +86,23 @@ public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLS
|
|||||||
private MappingNode toNodeTree(@NotNull final ConfigureSection section) {
|
private MappingNode toNodeTree(@NotNull final ConfigureSection section) {
|
||||||
List<NodeTuple> nodeTuples = new ArrayList<>();
|
List<NodeTuple> nodeTuples = new ArrayList<>();
|
||||||
for (final Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
|
for (final Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
|
||||||
|
Node keyNode = this.yaml.represent(entry.getKey());
|
||||||
final Node keyNode = this.yaml.represent(entry.getKey());
|
Node valueNode;
|
||||||
final Node valueNode;
|
|
||||||
if (entry.getValue() instanceof ConfigureSection) {
|
if (entry.getValue() instanceof ConfigureSection) {
|
||||||
valueNode = this.toNodeTree((ConfigureSection) entry.getValue());
|
valueNode = this.toNodeTree((ConfigureSection) entry.getValue());
|
||||||
} else {
|
} else {
|
||||||
valueNode = this.yaml.represent(entry.getValue());
|
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));
|
nodeTuples.add(new NodeTuple(keyNode, valueNode));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MappingNode(Tag.MAP, nodeTuples, DumperOptions.FlowStyle.BLOCK);
|
return new MappingNode(Tag.MAP, nodeTuples, DumperOptions.FlowStyle.BLOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CommentLine> buildComments(@NotNull CommentType type, @NotNull ConfigurationMetadata<List<String>> meta,
|
|
||||||
@Nullable String path) {
|
|
||||||
List<String> 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<CommentLine> buildComment(@NotNull CommentType type, @NotNull ConfigurationMetadata<String> 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
|
@NotNull
|
||||||
public String saveToString() {
|
public String saveToString(ConfigureSection section) {
|
||||||
|
MappingNode mappingNode = this.toNodeTree(section);
|
||||||
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));
|
|
||||||
|
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
if ((mappingNode.getBlockComments() == null || mappingNode.getBlockComments().isEmpty())
|
if ((mappingNode.getBlockComments() == null || mappingNode.getBlockComments().isEmpty())
|
||||||
&& (mappingNode.getEndComments() == null || mappingNode.getEndComments().isEmpty())
|
&& (mappingNode.getEndComments() == null || mappingNode.getEndComments().isEmpty())
|
||||||
@@ -137,13 +115,21 @@ public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLS
|
|||||||
}
|
}
|
||||||
this.yaml.serialize(mappingNode, writer);
|
this.yaml.serialize(mappingNode, writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return writer.toString();
|
return writer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save() throws Exception {
|
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
|
@Override
|
||||||
@@ -151,6 +137,11 @@ public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLS
|
|||||||
this.rootSection = fileReadString(this::loadFromString);
|
this.rootSection = fileReadString(this::loadFromString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.saveToString(section());
|
||||||
|
}
|
||||||
|
|
||||||
public @NotNull MemorySection loadFromString(@NotNull String data) throws Exception {
|
public @NotNull MemorySection loadFromString(@NotNull String data) throws Exception {
|
||||||
MappingNode mappingNode;
|
MappingNode mappingNode;
|
||||||
try (Reader reader = new UnicodeReader(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)))) {
|
try (Reader reader = new UnicodeReader(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)))) {
|
||||||
@@ -186,6 +177,70 @@ public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String serializeValue(@NotNull String key, @NotNull Object value) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put(key, value);
|
||||||
|
return saveToString(MemorySection.root(this, map));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Set<String> 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<String, String> pathComment = holder().metadata(key).get(CommentableMeta.INLINE);
|
||||||
|
if (pathComment == null || pathComment.isEmpty()) return null;
|
||||||
|
if (sectionKey == null) return pathComment.get(null);
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> 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<String> getHeaderComments(@Nullable String key) {
|
||||||
|
return holder().metadata(key).get(CommentableMeta.HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable List<String> getFooterComments(@Nullable String key) {
|
||||||
|
return holder().metadata(key).get(CommentableMeta.FOOTER);
|
||||||
|
}
|
||||||
|
|
||||||
public static class YamlRepresenter extends Representer {
|
public static class YamlRepresenter extends Representer {
|
||||||
|
|
||||||
public YamlRepresenter(@NotNull final DumperOptions dumperOptions) {
|
public YamlRepresenter(@NotNull final DumperOptions dumperOptions) {
|
||||||
@@ -213,7 +268,7 @@ public class YAMLSource extends FileConfigSource<MemorySection, Map<?, ?>, YAMLS
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void flattenMapping(@NotNull final MappingNode mappingNode) {
|
public void flattenMapping(@NotNull final MappingNode mappingNode) {
|
||||||
super.flattenMapping(mappingNode);
|
super.flattenMapping(mappingNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package sample;
|
|||||||
|
|
||||||
import cc.carm.lib.configuration.Configuration;
|
import cc.carm.lib.configuration.Configuration;
|
||||||
import cc.carm.lib.configuration.annotation.ConfigPath;
|
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.annotation.InlineComment;
|
||||||
import cc.carm.lib.configuration.value.standard.ConfiguredList;
|
import cc.carm.lib.configuration.value.standard.ConfiguredList;
|
||||||
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
||||||
@@ -10,12 +10,15 @@ import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ConfigPath(root = true)
|
@ConfigPath(root = true)
|
||||||
@HeaderComment("Configurations for sample")
|
@HeaderComments("Configurations for sample")
|
||||||
public interface SampleConfig extends Configuration {
|
public interface SampleConfig extends Configuration {
|
||||||
|
|
||||||
@InlineComment("Enabled?") // Inline comment
|
@InlineComment("Enabled?") // Inline comment
|
||||||
ConfiguredValue<Boolean> ENABLED = ConfiguredValue.of(true);
|
ConfiguredValue<Boolean> ENABLED = ConfiguredValue.of(true);
|
||||||
|
|
||||||
|
@HeaderComments("Server configurations") // Header comment
|
||||||
|
ConfiguredValue<Integer> PORT = ConfiguredValue.of(Integer.class);
|
||||||
|
|
||||||
ConfiguredList<UUID> UUIDS = ConfiguredList.builderOf(UUID.class).fromString()
|
ConfiguredList<UUID> UUIDS = ConfiguredList.builderOf(UUID.class).fromString()
|
||||||
.parse(UUID::fromString).serialize(UUID::toString)
|
.parse(UUID::fromString).serialize(UUID::toString)
|
||||||
.defaults(
|
.defaults(
|
||||||
@@ -26,11 +29,12 @@ public interface SampleConfig extends Configuration {
|
|||||||
@ConfigPath("info") // Custom path
|
@ConfigPath("info") // Custom path
|
||||||
interface INFO extends Configuration {
|
interface INFO extends Configuration {
|
||||||
|
|
||||||
@HeaderComment("Configure your name!") // Header comment
|
@HeaderComments("Configure your name!") // Header comment
|
||||||
ConfiguredValue<String> NAME = ConfiguredValue.of("Joker");
|
ConfiguredValue<String> NAME = ConfiguredValue.of("Joker");
|
||||||
|
|
||||||
@ConfigPath("year") // Custom path
|
@ConfigPath("how-old-are-you") // Custom path
|
||||||
ConfiguredValue<Integer> AGE = ConfiguredValue.of(24);
|
ConfiguredValue<Integer> AGE = ConfiguredValue.of(24);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ public class SampleTest {
|
|||||||
// 2. Initialize the configuration classes or instances.
|
// 2. Initialize the configuration classes or instances.
|
||||||
holder.initialize(SampleConfig.class);
|
holder.initialize(SampleConfig.class);
|
||||||
// 3. Enjoy using the configuration!
|
// 3. Enjoy using the configuration!
|
||||||
|
System.out.println("Enabled? -> " + SampleConfig.ENABLED.resolve());
|
||||||
SampleConfig.ENABLED.set(false);
|
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() + ")!");
|
System.out.println("Your name is " + SampleConfig.INFO.NAME.resolve() + " (age=" + SampleConfig.INFO.AGE.resolve() + ")!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<String, List<String>> 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<String, List<String>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user