1
mirror of https://github.com/CarmJos/EasyConfiguration.git synced 2026-06-04 18:48:20 +08:00

feat(loader): Refactor loaders and metadata.

This commit is contained in:
2024-01-16 23:25:21 +08:00
parent 2df33e3458
commit b912ea369c
33 changed files with 632 additions and 332 deletions
+13
View File
@@ -18,6 +18,19 @@
<artifactId>easyconfiguration-core</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>cc.carm.lib</groupId>
<artifactId>easyoptions</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>cc.carm.lib</groupId>
<artifactId>easyannotation</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
@@ -1,7 +1,6 @@
package cc.carm.lib.configuration.adapter;
import cc.carm.lib.configuration.source.ConfigurationProvider;
import org.jetbrains.annotations.NotNull;
/**
* Value adapter, used to convert the value of the configuration file into the objects.
@@ -10,7 +9,8 @@ import org.jetbrains.annotations.NotNull;
* @param <B> The type of the base data
* @param <V> The type of the target value
*/
public abstract class ValueAdapter<P extends ConfigurationProvider, B, V> {
public abstract class ValueAdapter<P extends ConfigurationProvider, B, V>
implements ValueSerializer<P, B, V>, ValueDeserializer<P, B, V> {
protected final Class<? super B> baseType;
protected final Class<? super V> valueType;
@@ -28,10 +28,6 @@ public abstract class ValueAdapter<P extends ConfigurationProvider, B, V> {
return valueType;
}
public abstract B serialize(@NotNull P provider, @NotNull V value) throws Exception;
public abstract V deserialize(@NotNull P provider, @NotNull Class<? extends V> clazz, @NotNull B data) throws Exception;
public boolean isAdaptedFrom(Class<?> clazz) {
return clazz.isAssignableFrom(valueType);
}
@@ -41,7 +37,7 @@ public abstract class ValueAdapter<P extends ConfigurationProvider, B, V> {
}
public boolean isAdapterOf(Class<?> clazz) {
return valueType.isAssignableFrom(clazz);
return clazz == valueType;
}
@SuppressWarnings("unchecked")
@@ -3,20 +3,17 @@ package cc.carm.lib.configuration.adapter;
import cc.carm.lib.configuration.adapter.strandard.PrimitiveAdapters;
import cc.carm.lib.configuration.core.function.ConfigDataFunction;
import cc.carm.lib.configuration.source.ConfigurationProvider;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
public class ValueAdapterRegistry<P extends ConfigurationProvider> {
protected final @NotNull P provider;
protected final Map<Class<?>, ValueAdapter<P, ?, ?>> adapters = new HashMap<>();
public ValueAdapterRegistry(@NotNull P provider) {
this.provider = provider;
}
public void register(@NotNull ValueAdapter<P, ?, ?> adapter) {
adapters.put(adapter.getValueClass(), adapter);
}
@@ -46,30 +43,31 @@ public class ValueAdapterRegistry<P extends ConfigurationProvider> {
}
@SuppressWarnings("unchecked")
public <T> T deserialize(Class<T> type, Object value) throws Exception {
if (value == null) return null;
if (type == Object.class) return type.cast(value);
@Contract("_,_,null -> null")
public <T> T deserialize(@NotNull P provider, @NotNull Class<T> type, @Nullable Object source) throws Exception {
if (source == null) return null; // Null check
if (type.isInstance(source)) return type.cast(source); // Not required to deserialize
ValueAdapter<P, ?, ?> adapter = getAdapter(type);
if (adapter == null) throw new RuntimeException("No adapter for type " + type.getName());
// CHECK IF VALUE IS ADAPTED FROM GIVEN VALUE'S TYPE
if (adapter.isAdaptedFrom(value)) {
return (T) adapter.deserializeObject(provider, type, value);
// Check if value is adapted from given value's type
if (adapter.isAdaptedFrom(source)) {
return (T) adapter.deserializeObject(provider, type, source);
}
// OTHERWISE, WE NEED TO DESERIALIZE ONE BY ONE
Object baseValue = deserialize(adapter.getBaseClass(), value);
// Otherwise, we need to deserialize one by one.
Object baseValue = deserialize(provider, adapter.getBaseClass(), source);
if (baseValue == null) return null; // Null check
return (T) adapter.deserializeObject(provider, type, baseValue);
}
public <T> Object serialize(T value) throws Exception {
if (value == null) return null;
@Contract("_,null -> null")
public <T> Object serialize(@NotNull P provider, @Nullable T value) throws Exception {
if (value == null) return null; // Null check
Class<?> valueClass = value.getClass();
ValueAdapter<P, ?, ?> adapter = adapters.get(valueClass);
ValueAdapter<P, ?, ?> adapter = getAdapter(value.getClass());
if (adapter == null) return value; // No adapters, try to return the original value
if (adapter instanceof PrimitiveAdapters) {
@@ -79,12 +77,14 @@ public class ValueAdapterRegistry<P extends ConfigurationProvider> {
}
// Otherwise, we need to serialize one by one.
return serialize(adapter.serializeObject(provider, value));
return serialize(provider, adapter.serializeObject(provider, value));
}
public ValueAdapter<P, ?, ?> getAdapter(Class<?> clazz) {
ValueAdapter<P, ?, ?> byClass = adapters.get(clazz);
if (byClass != null) return byClass;
return adapters.getOrDefault(clazz, findAdapter(clazz));
}
public ValueAdapter<P, ?, ?> findAdapter(Class<?> clazz) {
return adapters.values().stream().filter(adapter -> adapter.isAdapterOf(clazz)).findFirst().orElse(null);
}
@@ -0,0 +1,18 @@
package cc.carm.lib.configuration.adapter;
import cc.carm.lib.configuration.source.ConfigurationProvider;
import org.jetbrains.annotations.NotNull;
/**
* Value deserializer, convert base data to target value.
*
* @param <P> Configuration provider
* @param <B> The type of base data
* @param <V> The type of target value
*/
@FunctionalInterface
public interface ValueDeserializer<P extends ConfigurationProvider, B, V> {
V deserialize(@NotNull P provider, @NotNull Class<? extends V> clazz, @NotNull B data) throws Exception;
}
@@ -0,0 +1,18 @@
package cc.carm.lib.configuration.adapter;
import cc.carm.lib.configuration.source.ConfigurationProvider;
import org.jetbrains.annotations.NotNull;
/**
* Value serializer, convert target value to base data.
*
* @param <P> Configuration provider
* @param <B> The type of base data
* @param <V> The type of value
*/
@FunctionalInterface
public interface ValueSerializer<P extends ConfigurationProvider, B, V> {
B serialize(@NotNull P provider, @NotNull V value) throws Exception;
}
@@ -21,4 +21,9 @@ public class EnumAdapter<P extends ConfigurationProvider> extends ValueAdapter<P
return Enum.valueOf(clazz, data);
}
@Override
public boolean isAdapterOf(Class<?> clazz) {
return clazz.isEnum();
}
}
@@ -1,7 +1,5 @@
package cc.carm.lib.configuration.annotation;
import cc.carm.lib.configuration.core.ConfigInitializer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -16,7 +14,7 @@ public @interface ConfigPath {
/**
* The path value of the current configuration.
* If not set,will generate the path by {@link ConfigInitializer#getPathFromName(String)}.
* If not set,will generate the path by {@link cc.carm.lib.configuration.source.path.PathGenerator}.
*
* @return The path value of the current configuration
*/
@@ -1,40 +0,0 @@
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;
/**
* Header Comments.
* Add a comment to the top of the corresponding configuration for easy reading and viewing.
* <p>e.g.
* <blockquote><pre>
* # The first line of the comment
* # The second line of the comment
* foo: "bar"
* </pre></blockquote>
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HeaderComment {
/**
* If the content of the note is 0, it will be treated as a blank line.
* <p> e.g. <b>{"foo","","bar"}</b>
* Will be set as
* <blockquote><pre>
* # foo
*
* # bar
* foo: "bar"
* </pre></blockquote>
*
* @return The content of this comment
*/
@NotNull
String[] value() default "";
}
@@ -1,34 +0,0 @@
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;
/**
* Inline comments,
* add comments to the rows of the corresponding configurations for easy reading and viewing.
* e.g.
* <blockquote><pre>
* foo: "bar" # Comment Contents
* </pre></blockquote>
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InlineComment {
/**
* If the content length is 0, the comment will not be added.
* <p> e.g. <b>"foobar"</b> will be set
* <blockquote><pre>
* foo: "bar" # foobar
* </pre></blockquote>
*
* @return The content of this comment
*/
@NotNull
String value() default "";
}
@@ -1,275 +0,0 @@
package cc.carm.lib.configuration.core;
import cc.carm.lib.configuration.annotation.ConfigPath;
import cc.carm.lib.configuration.annotation.HeaderComment;
import cc.carm.lib.configuration.annotation.InlineComment;
import cc.carm.lib.configuration.core.source.ConfigurationProvider;
import cc.carm.lib.configuration.value.ConfigValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
/**
* 配置文件类初始化方法
* 用于初始化 {@link Configuration} 中的每个 {@link ConfigValue} 对象
*
* @param <T> {@link ConfigurationProvider} 配置文件的数据来源
* @author CarmJos
*/
public class ConfigInitializer<T extends ConfigurationProvider<?>> {
protected final @NotNull T provider;
public ConfigInitializer(@NotNull T provider) {
this.provider = provider;
}
/**
* 初始化指定类以及其子类的所有 {@link ConfigValue} 对象。
*
* @param clazz 配置文件类,须继承于 {@link Configuration} 。
* @param saveDefaults 是否写入默认值(默认为 true)。
*/
public void initialize(@NotNull Class<? extends Configuration> clazz, boolean saveDefaults) {
initialize(clazz, saveDefaults, true);
}
/**
* 初始化指定类的所有 {@link ConfigValue} 对象。
*
* @param clazz 配置文件类,须继承于 {@link Configuration} 。
* @param saveDefaults 是否写入默认值(默认为 true)。
* @param loadSubClasses 是否加载内部子类(默认为 true)。
*/
public void initialize(@NotNull Class<? extends Configuration> clazz,
boolean saveDefaults, boolean loadSubClasses) {
initializeStaticClass(
clazz, null, null,
null, null, null,
saveDefaults, loadSubClasses
);
if (saveDefaults) {
try {
provider.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 初始化指定实例的所有 {@link ConfigValue} 与内部 {@link Configuration} 对象。
*
* @param config 配置文件实例类,须实现 {@link Configuration} 。
*/
public void initialize(@NotNull Configuration config) {
initialize(config, true);
}
/**
* 初始化指定实例的所有 {@link ConfigValue} 与内部 {@link Configuration} 对象。
*
* @param config 配置文件实例类,须实现 {@link Configuration} 。
* @param saveDefaults 是否写入默认值(默认为 true)。
*/
public void initialize(@NotNull Configuration config, boolean saveDefaults) {
initializeInstance(
config, null, null,
null, null, null,
saveDefaults
);
if (saveDefaults) {
try {
provider.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 针对实例类的初始化方法
private void initializeInstance(@NotNull Configuration root,
@Nullable String parentPath, @Nullable String fieldName,
@Nullable ConfigPath fieldPath,
@Nullable HeaderComment fieldHeaderComments,
@Nullable InlineComment fieldInlineComments,
boolean saveDefaults) {
String path = getClassPath(root.getClass(), parentPath, fieldName, fieldPath);
this.provider.setHeaderComment(path, getClassHeaderComments(root.getClass(), fieldHeaderComments));
if (path != null) this.provider.setInlineComment(path, readInlineComments(fieldInlineComments));
for (Field field : root.getClass().getDeclaredFields()) {
initializeField(root, field, path, saveDefaults, false);
}
}
// 针对静态类的初始化方法
private void initializeStaticClass(@NotNull Class<?> clazz,
@Nullable String parentPath, @Nullable String fieldName,
@Nullable ConfigPath fieldPath,
@Nullable HeaderComment fieldHeaderComments,
@Nullable InlineComment fieldInlineComments,
boolean saveDefaults, boolean loadSubClasses) {
if (!Configuration.class.isAssignableFrom(clazz)) return; // 只解析继承了 ConfigurationRoot 的类
String path = getClassPath(clazz, parentPath, fieldName, fieldPath);
this.provider.setHeaderComment(path, getClassHeaderComments(clazz, fieldHeaderComments));
if (path != null) this.provider.setInlineComment(path, readInlineComments(fieldInlineComments));
for (Field field : clazz.getDeclaredFields()) {
initializeField(clazz, field, path, saveDefaults, loadSubClasses);
}
if (!loadSubClasses) return;
Class<?>[] classes = clazz.getDeclaredClasses();
for (int i = classes.length - 1; i >= 0; i--) { // 逆向加载,保持顺序。
initializeStaticClass(
classes[i], path, classes[i].getSimpleName(),
null, null, null,
saveDefaults, true
);
}
}
private void initializeField(@NotNull Object source, @NotNull Field field,
@Nullable String parent, boolean saveDefaults, boolean loadSubClasses) {
try {
field.setAccessible(true);
Object object = field.get(source);
if (object instanceof ConfigValue<?>) {
initializeValue(
(ConfigValue<?>) object, getFieldPath(field, parent),
field.getAnnotation(HeaderComment.class),
field.getAnnotation(InlineComment.class),
saveDefaults
);
} else if (source instanceof Configuration && object instanceof Configuration) {
// 当且仅当 源字段与字段 均为ConfigurationRoot实例时,才对目标字段进行下一步初始化加载。
initializeInstance(
(Configuration) object, parent, field.getName(),
field.getAnnotation(ConfigPath.class),
field.getAnnotation(HeaderComment.class),
field.getAnnotation(InlineComment.class),
saveDefaults
);
} else if (source instanceof Class<?> && object instanceof Class<?>) {
// 当且仅当 源字段与字段 均为静态类时,才对目标字段进行下一步初始化加载。
initializeStaticClass(
(Class<?>) object, parent, field.getName(),
field.getAnnotation(ConfigPath.class),
field.getAnnotation(HeaderComment.class),
field.getAnnotation(InlineComment.class),
saveDefaults, loadSubClasses
);
}
// 以上判断实现以下规范:
// - 实例类中仅加载 ConfigValue实例 与 ConfigurationRoot实例
// - 静态类中仅加载 静态ConfigValue实例 与 静态ConfigurationRoot类
} catch (IllegalAccessException ignored) {
}
}
protected void initializeValue(@NotNull ConfigValue<?> value, @NotNull String path,
@Nullable HeaderComment fieldHeaderComment,
@Nullable InlineComment fieldInlineComment,
boolean saveDefaults) {
value.initialize(
provider, saveDefaults, path,
readHeaderComments(fieldHeaderComment),
readInlineComments(fieldInlineComment)
);
}
protected static @Nullable List<String> getClassHeaderComments(@NotNull Class<?> clazz,
@Nullable HeaderComment fieldAnnotation) {
List<String> classComments = readHeaderComments(clazz.getAnnotation(HeaderComment.class));
if (classComments != null) return classComments;
else return readHeaderComments(fieldAnnotation);
}
protected static List<String> readHeaderComments(@Nullable HeaderComment annotation) {
if (annotation == null) return null;
String[] value = annotation.value();
return value.length > 0 ? Arrays.asList(value) : null;
}
protected static @Nullable String readInlineComments(@Nullable InlineComment annotation) {
if (annotation == null) return null;
String value = annotation.value();
return value.length() > 0 ? value : null;
}
protected static @Nullable String getClassPath(@Nullable Class<?> clazz,
@Nullable String parentPath,
@Nullable String filedName,
@Nullable ConfigPath fieldAnnotation) {
@NotNull String parent = parentPath != null ? parentPath + "." : "";
boolean fromRoot = false;
// 先获取 Class 对应的路径注解 其优先度最高。
if (clazz != null) {
ConfigPath clazzAnnotation = clazz.getAnnotation(ConfigPath.class);
if (clazzAnnotation != null) {
fromRoot = clazzAnnotation.root();
if (clazzAnnotation.value().length() > 0) {
return (fromRoot ? "" : parent) + clazzAnnotation.value();
}
}
}
if (fieldAnnotation != null) {
fromRoot = fromRoot || fieldAnnotation.root();
if (fieldAnnotation.value().length() > 0) {
return (fromRoot ? "" : parent) + fieldAnnotation.value();
}
}
// 再由 fieldName 获取路径
if (filedName != null) return (fromRoot ? "" : parent) + getPathFromName(filedName);
else return null; // 不满足上述条件 且 无 fieldName,则说明是根路径。
}
protected static @NotNull String getFieldPath(@NotNull Field field, @Nullable String parentPath) {
@NotNull String parent = parentPath != null ? parentPath + "." : "";
boolean fromRoot = false;
// 先获取 Field 对应的路径注解 其优先度最高。
ConfigPath pathAnnotation = field.getAnnotation(ConfigPath.class);
if (pathAnnotation != null) {
fromRoot = pathAnnotation.root();
if (pathAnnotation.value().length() > 0) {
return (fromRoot ? "" : parent) + pathAnnotation.value();
}
}
// 最后再通过 fieldName 自动生成路径
return (fromRoot ? "" : parent) + getPathFromName(field.getName());
}
/**
* 得到指定元素的配置名称。
* 采用 全小写,以“-”链接 的命名规则。
*
* @param name 源名称
* @return 全小写,以“-”链接 的 路径名称
*/
public static String getPathFromName(String name) {
return name.replaceAll("[A-Z]", "-$0") // 将驼峰转换为蛇形;
.replaceAll("-(.*)", "$1") // 若首字母也为大写,则也会被转换,需要去掉第一个横线
.replaceAll("_-([A-Z])", "_$1") // 因为命名中可能包含 _,因此需要被特殊处理一下
.replaceAll("([a-z])-([A-Z])", "$1_$2") // 然后将非全大写命名的内容进行转换
.replace("-", "") // 移除掉多余的横线
.replace("_", "-") // 将下划线替换为横线
.toLowerCase(); // 最后转为全小写
}
}
@@ -5,4 +5,5 @@ package cc.carm.lib.configuration.core;
* which is used to label and record the configuration information.
*/
public interface Configuration {
}
@@ -1,51 +0,0 @@
package cc.carm.lib.configuration.core.source;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
public class ConfigurationComments {
protected final @NotNull Map<String, List<String>> headerComments = new HashMap<>();
protected final @NotNull Map<String, String> inlineComments = new HashMap<>();
protected @NotNull Map<String, List<String>> getHeaderComments() {
return headerComments;
}
protected @NotNull Map<String, String> getInlineComments() {
return inlineComments;
}
public void setHeaderComments(@Nullable String path, @Nullable List<String> comments) {
if (comments == null) {
getHeaderComments().remove(path);
} else {
getHeaderComments().put(path, comments);
}
}
public void setInlineComment(@NotNull String path, @Nullable String comment) {
if (comment == null) {
getInlineComments().remove(path);
} else {
getInlineComments().put(path, comment);
}
}
@Nullable
@Unmodifiable
public List<String> getHeaderComment(@Nullable String path) {
return Optional.ofNullable(getHeaderComments().get(path)).map(Collections::unmodifiableList).orElse(null);
}
public @Nullable String getInlineComment(@NotNull String path) {
return getInlineComments().get(path);
}
}
@@ -1,7 +1,7 @@
package cc.carm.lib.configuration.core.source;
import cc.carm.lib.configuration.core.ConfigInitializer;
import cc.carm.lib.configuration.core.Configuration;
import cc.carm.lib.configuration.source.comment.ConfigurationComments;
import cc.carm.lib.configuration.value.ConfigValue;
import cc.carm.lib.configuration.value.impl.CachedConfigValue;
import org.jetbrains.annotations.NotNull;
@@ -0,0 +1,8 @@
package cc.carm.lib.configuration.manifest;
public class ValueManifest {
}
@@ -1,15 +1,85 @@
package cc.carm.lib.configuration.source;
import cc.carm.lib.configuration.adapter.ValueAdapter;
import cc.carm.lib.configuration.adapter.ValueAdapterRegistry;
import cc.carm.lib.configuration.core.function.ConfigDataFunction;
import cc.carm.lib.configuration.source.path.PathGenerator;
import cc.carm.lib.easyoptions.OptionHolder;
import cc.carm.lib.easyoptions.OptionType;
import org.jetbrains.annotations.NotNull;
public abstract class ConfigurationBuilder<P extends ConfigurationProvider> {
import java.util.function.Consumer;
import java.util.function.Function;
protected ConfigurationLoader loader;
protected ValueAdapterRegistry<P> processors;
public abstract class ConfigurationBuilder<P extends ConfigurationProvider<P>, C> {
protected Function<P, ConfigurationLoader<P>> loaderFunction = ConfigurationLoader::new;
protected Consumer<ConfigurationLoader<P>> loaderConsumer = loader -> {
};
public abstract @NotNull ConfigurationProvider build();
protected ValueAdapterRegistry<P> adapters = new ValueAdapterRegistry<>();
protected OptionHolder options = new OptionHolder();
public abstract C getThis();
public C loader(Function<P, ConfigurationLoader<P>> loaderFunction) {
this.loaderFunction = loaderFunction;
return getThis();
}
public C loader(ConfigurationLoader<P> loader) {
return loader(p -> loader);
}
public C loader(Consumer<ConfigurationLoader<P>> loaderConsumer) {
this.loaderConsumer = this.loaderConsumer.andThen(loaderConsumer);
return getThis();
}
public C pathGenerator(PathGenerator<P> pathGenerator) {
return loader(loader -> {
loader.setPathGenerator(pathGenerator);
});
}
public C adapters(ValueAdapterRegistry<P> adapters) {
this.adapters = adapters;
return getThis();
}
public C adapter(Consumer<ValueAdapterRegistry<P>> adapterRegistryConsumer) {
adapterRegistryConsumer.accept(adapters);
return getThis();
}
public C adapter(@NotNull ValueAdapter<P, ?, ?> adapter) {
return adapter(a -> a.register(adapter));
}
public <T> C adapter(Class<T> clazz, @NotNull ValueAdapter<P, ?, T> adapter) {
return adapter(a -> a.register(clazz, adapter));
}
public <B, V> C adapter(Class<B> baseClass, Class<V> valueClass,
ConfigDataFunction<B, V> parser, ConfigDataFunction<V, B> serializer) {
return adapter(a -> a.register(baseClass, valueClass, parser, serializer));
}
public C options(OptionHolder options) {
this.options = options;
return getThis();
}
public C option(Consumer<OptionHolder> optionsConsumer) {
optionsConsumer.accept(options);
return getThis();
}
public <O> C option(OptionType<O> option, O value) {
return option(o -> o.set(option, value));
}
public abstract @NotNull P build();
}
@@ -1,4 +1,196 @@
package cc.carm.lib.configuration.source;
public interface ConfigurationLoader {
import cc.carm.lib.configuration.annotation.ConfigPath;
import cc.carm.lib.configuration.core.Configuration;
import cc.carm.lib.configuration.source.path.PathGenerator;
import cc.carm.lib.configuration.source.path.StandardPathGenerator;
import cc.carm.lib.configuration.value.ConfigValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
/**
* Configuration loader,
* used to load configuration values from {@link cc.carm.lib.configuration.core.Configuration} classes.
*/
public class ConfigurationLoader<P extends ConfigurationProvider<P>> {
protected final P provider;
protected PathGenerator<P> pathGenerator;
public ConfigurationLoader(P provider) {
this(provider, StandardPathGenerator.of(provider));
}
public ConfigurationLoader(P provider, PathGenerator<P> pathGenerator) {
this.provider = provider;
this.pathGenerator = pathGenerator;
}
public void setPathGenerator(PathGenerator<P> pathGenerator) {
this.pathGenerator = pathGenerator;
}
public PathGenerator<P> getPathGenerator() {
return pathGenerator;
}
/**
* 初始化指定类以及其子类的所有 {@link ConfigValue} 对象。
*
* @param clazz 配置文件类,须继承于 {@link Configuration} 。
*/
public void initialize(@NotNull P provider, @NotNull Class<? extends Configuration> clazz) {
initialize(clazz, saveDefaults, true);
}
/**
* 初始化指定类的所有 {@link ConfigValue} 对象。
*
* @param clazz 配置文件类,须继承于 {@link Configuration} 。
* @param saveDefaults 是否写入默认值(默认为 true)。
* @param loadSubClasses 是否加载内部子类(默认为 true)。
*/
public void initialize(@NotNull Class<? extends Configuration> clazz) {
initializeStaticClass(
clazz, null, null,
null, null, null,
saveDefaults, loadSubClasses
);
if (saveDefaults) {
try {
provider.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 初始化指定实例的所有 {@link ConfigValue} 与内部 {@link Configuration} 对象。
*
* @param config 配置文件实例类,须实现 {@link Configuration} 。
*/
public void initialize(@NotNull Configuration config) {
initialize(config, true);
}
/**
* 初始化指定实例的所有 {@link ConfigValue} 与内部 {@link Configuration} 对象。
*
* @param config 配置文件实例类,须实现 {@link Configuration} 。
* @param saveDefaults 是否写入默认值(默认为 true)。
*/
public void initialize(@NotNull Configuration config, boolean saveDefaults) {
initializeInstance(
config, null, null,
null, null, null,
saveDefaults
);
if (saveDefaults) {
try {
provider.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 针对实例类的初始化方法
private void initializeInstance(@NotNull Configuration root,
@Nullable String parentPath, @Nullable String fieldName,
@Nullable ConfigPath fieldPath,
@Nullable HeaderComment fieldHeaderComments,
@Nullable InlineComment fieldInlineComments,
boolean saveDefaults) {
String path = getClassPath(root.getClass(), parentPath, fieldName, fieldPath);
this.provider.setHeaderComment(path, getClassHeaderComments(root.getClass(), fieldHeaderComments));
if (path != null) this.provider.setInlineComment(path, readInlineComments(fieldInlineComments));
for (Field field : root.getClass().getDeclaredFields()) {
initializeField(root, field, path, saveDefaults, false);
}
}
// 针对静态类的初始化方法
private void initializeStaticClass(@NotNull Class<?> clazz,
@Nullable String parentPath, @Nullable String fieldName,
@Nullable ConfigPath fieldPath,
@Nullable HeaderComment fieldHeaderComments,
@Nullable InlineComment fieldInlineComments,
boolean saveDefaults, boolean loadSubClasses) {
if (!Configuration.class.isAssignableFrom(clazz)) return; // 只解析继承了 ConfigurationRoot 的类
String path = getClassPath(clazz, parentPath, fieldName, fieldPath);
this.provider.setHeaderComment(path, getClassHeaderComments(clazz, fieldHeaderComments));
if (path != null) this.provider.setInlineComment(path, readInlineComments(fieldInlineComments));
for (Field field : clazz.getDeclaredFields()) {
initializeField(clazz, field, path, saveDefaults, loadSubClasses);
}
if (!loadSubClasses) return;
Class<?>[] classes = clazz.getDeclaredClasses();
for (int i = classes.length - 1; i >= 0; i--) { // 逆向加载,保持顺序。
initializeStaticClass(
classes[i], path, classes[i].getSimpleName(),
null, null, null,
saveDefaults, true
);
}
}
private void initializeField(@NotNull Object source, @NotNull Field field,
@Nullable String parent, boolean saveDefaults, boolean loadSubClasses) {
try {
field.setAccessible(true);
Object object = field.get(source);
if (object instanceof ConfigValue<?>) {
initializeValue(
(ConfigValue<?>) object, getFieldPath(field, parent),
field.getAnnotation(HeaderComment.class),
field.getAnnotation(InlineComment.class),
saveDefaults
);
} else if (source instanceof Configuration && object instanceof Configuration) {
// 当且仅当 源字段与字段 均为ConfigurationRoot实例时,才对目标字段进行下一步初始化加载。
initializeInstance(
(Configuration) object, parent, field.getName(),
field.getAnnotation(ConfigPath.class),
field.getAnnotation(HeaderComment.class),
field.getAnnotation(InlineComment.class),
saveDefaults
);
} else if (source instanceof Class<?> && object instanceof Class<?>) {
// 当且仅当 源字段与字段 均为静态类时,才对目标字段进行下一步初始化加载。
initializeStaticClass(
(Class<?>) object, parent, field.getName(),
field.getAnnotation(ConfigPath.class),
field.getAnnotation(HeaderComment.class),
field.getAnnotation(InlineComment.class),
saveDefaults, loadSubClasses
);
}
// 以上判断实现以下规范:
// - 实例类中仅加载 ConfigValue实例 与 ConfigurationRoot实例
// - 静态类中仅加载 静态ConfigValue实例 与 静态ConfigurationRoot类
} catch (IllegalAccessException ignored) {
}
}
protected void initializeValue(@NotNull ConfigValue<?> value, @NotNull String path,
@Nullable HeaderComment fieldHeaderComment,
@Nullable InlineComment fieldInlineComment,
boolean saveDefaults) {
value.initialize(
provider, saveDefaults, path,
readHeaderComments(fieldHeaderComment),
readInlineComments(fieldInlineComment)
);
}
}
@@ -1,6 +1,37 @@
package cc.carm.lib.configuration.source;
public class ConfigurationProvider {
import cc.carm.lib.configuration.adapter.ValueAdapterRegistry;
import cc.carm.lib.configuration.core.Configuration;
import cc.carm.lib.easyoptions.OptionHolder;
import cc.carm.lib.easyoptions.OptionType;
import org.jetbrains.annotations.NotNull;
public abstract class ConfigurationProvider<P extends ConfigurationProvider<P>> {
protected @NotNull ConfigurationLoader<P> loader = new ConfigurationLoader<>();
protected @NotNull ValueAdapterRegistry<P> adapters = new ValueAdapterRegistry<>();
protected @NotNull OptionHolder options = new OptionHolder();
public OptionHolder options() {
return options;
}
public <T> @NotNull T option(@NotNull OptionType<T> option) {
return options.get(option);
}
public <T> void option(@NotNull OptionType<T> option, @NotNull T value) {
options.set(option, value);
}
public ConfigurationLoader<P> loader() {
return loader;
}
public void load(Configuration configuration) {
loader().load(configuration);
}
}
@@ -0,0 +1,33 @@
package cc.carm.lib.configuration.source.path;
import cc.carm.lib.configuration.source.ConfigurationProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
public interface PathGenerator<P extends ConfigurationProvider<P>> {
@Nullable String getFieldPath(@Nullable String parentPath, @NotNull Field field);
@Nullable String getClassPath(@Nullable String parentPath,
@NotNull Class<?> clazz, @Nullable Field clazzField);
/**
* Get the configuration name of the specified element.
* Use the naming convention of all lowercase and "-" links.
*
* @param name source name
* @return the final path
*/
static String covertPathName(String name) {
return name.replaceAll("[A-Z]", "-$0") // 将驼峰形转换为蛇形;
.replaceAll("-(.*)", "$1") // 若首字母也为大写,则也会被转换,需要去掉第一个横线
.replaceAll("_-([A-Z])", "_$1") // 因为命名中可能包含 _,因此需要被特殊处理一下
.replaceAll("([a-z])-([A-Z])", "$1_$2") // 然后将非全大写命名的内容进行转换
.replace("-", "") // 移除掉多余的横线
.replace("_", "-") // 将下划线替换为横线
.toLowerCase(); // 最后转为全小写
}
}
@@ -0,0 +1,74 @@
package cc.carm.lib.configuration.source.path;
import cc.carm.lib.configuration.annotation.ConfigPath;
import cc.carm.lib.configuration.source.ConfigurationProvider;
import cc.carm.lib.configuration.source.standard.ConfigurationOptions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.function.UnaryOperator;
public class StandardPathGenerator<P extends ConfigurationProvider<P>> implements PathGenerator<P> {
public static <T extends ConfigurationProvider<T>> StandardPathGenerator<T> of(T provider) {
return of(provider, PathGenerator::covertPathName);
}
public static <T extends ConfigurationProvider<T>> StandardPathGenerator<T> of(T provider, UnaryOperator<String> pathConverter) {
return new StandardPathGenerator<>(provider, pathConverter);
}
protected final P provider;
protected UnaryOperator<String> pathConverter;
public StandardPathGenerator(P provider, UnaryOperator<String> pathConverter) {
this.provider = provider;
this.pathConverter = pathConverter;
}
public @NotNull UnaryOperator<String> getPathConverter() {
return pathConverter;
}
public void setPathConverter(UnaryOperator<String> pathConverter) {
this.pathConverter = pathConverter;
}
public String covertPath(String name) {
return pathConverter.apply(name);
}
public char pathSeparator() {
return provider.option(ConfigurationOptions.PATH_SEPARATOR);
}
protected String link(@Nullable String parent, boolean root, @Nullable String path) {
if (path == null || path.isEmpty()) return root ? null : parent;
return root && parent != null ? covertPath(path) : parent + pathSeparator() + covertPath(path);
}
@Override
public @Nullable String getFieldPath(@Nullable String parentPath, @NotNull Field field) {
ConfigPath path = field.getAnnotation(ConfigPath.class);
if (path == null) return link(parentPath, false, field.getName()); // No annotation, use field name.
else return link(parentPath, path.root(), path.value().isEmpty() ? field.getName() : path.value());
}
@Override
public @Nullable String getClassPath(@Nullable String parentPath, @NotNull Class<?> clazz, @Nullable Field clazzField) {
// For standard path generator, we generate path following by:
// 1. Check if the class has a ConfigPath annotation, if so, use the root and value as the path.
// 2. If the class defined as a field, check if the field has a ConfigPath annotation,
// and use filed information.
ConfigPath clazzPath = clazz.getAnnotation(ConfigPath.class);
if (clazzPath != null) return link(parentPath, clazzPath.root(), clazzPath.value());
if (clazzField == null) return parentPath; // No field, return same as parent.
ConfigPath fieldPath = clazzField.getAnnotation(ConfigPath.class);
if (fieldPath == null) return link(parentPath, false, clazzField.getName());
else return getFieldPath(parentPath, clazzField);
}
}
@@ -0,0 +1,12 @@
package cc.carm.lib.configuration.source.standard;
import cc.carm.lib.configuration.annotation.ConfigPath;
import cc.carm.lib.easyannotation.AnnotatedMetaType;
public interface ConfigurationMetaTypes {
AnnotatedMetaType<ConfigPath, String> PATH = AnnotatedMetaType.of(ConfigPath.class, ConfigPath::value);
AnnotatedMetaType<ConfigPath, Boolean> ROOT = AnnotatedMetaType.of(ConfigPath.class, ConfigPath::root);
}
@@ -0,0 +1,30 @@
package cc.carm.lib.configuration.source.standard;
import cc.carm.lib.easyoptions.OptionType;
import static cc.carm.lib.easyoptions.OptionType.of;
public interface ConfigurationOptions {
/**
* The configuration path separator.
*/
OptionType<Character> PATH_SEPARATOR = of('.');
/**
* Whether to copy files from resource if exists.
*/
OptionType<Boolean> COPY_DEFAULTS = of(true);
/**
* Whether to save default values if offered and not exists in configuration.
*/
OptionType<Boolean> SAVE_DEFAULTS = of(true);
/**
* Whether to load subclasses of configuration class.
*/
OptionType<Boolean> LOAD_SUB_CLASSES = of(true);
}
@@ -1,6 +1,5 @@
package cc.carm.lib.configuration.value;
import cc.carm.lib.configuration.core.ConfigInitializer;
import cc.carm.lib.configuration.core.source.ConfigurationProvider;
import cc.carm.lib.configuration.core.source.ConfigurationWrapper;
import org.jetbrains.annotations.NotNull;
+8 -5
View File
@@ -12,7 +12,7 @@ public class AdaptTest {
@Test
public void test() throws Exception {
ValueAdapterRegistry<?> registry = new ValueAdapterRegistry<>(new ConfigurationProvider());
ValueAdapterRegistry<ConfigurationProvider> registry = new ValueAdapterRegistry<>();
registry.register(Long.class, PrimitiveAdapters.ofLong());
registry.register(long.class, PrimitiveAdapters.ofLong());
registry.register(Integer.class, PrimitiveAdapters.ofInteger());
@@ -36,16 +36,19 @@ public class AdaptTest {
registry.register(
Duration.class, LocalTime.class,
duration -> LocalTime.now().plus(duration),
data -> Duration.between(data, LocalTime.now())
data -> Duration.between(LocalTime.now(), data)
);
LocalTime v = registry.deserialize(LocalTime.class, "600");
Object d = registry.serialize(v);
ConfigurationProvider provider = new ConfigurationProvider();
LocalTime v = registry.deserialize(provider, LocalTime.class, "600");
Object d = registry.serialize(provider, v);
System.out.println(v);
System.out.println(d);
System.out.println(registry.deserialize(TestEnum.class, "b"));
System.out.println(registry.deserialize(provider, TestEnum.class, "b"));
System.out.println(registry.serialize(provider, TestEnum.C).getClass());
}
enum TestEnum {
-1
View File
@@ -1,4 +1,3 @@
import cc.carm.lib.configuration.core.ConfigInitializer;
import org.junit.Test;
public class NameTest {