From 3a0a8e79b98a61f63452bb3045886dcfd501fddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AC=E8=8A=B1ice?= <67498111+flowerinsnowdh@users.noreply.github.com> Date: Wed, 21 Jun 2023 19:54:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(hocon):=20=E5=AE=8C=E6=88=90=E5=AF=B9=20HO?= =?UTF-8?q?CON=20=E6=A0=BC=E5=BC=8F=E7=9A=84=E6=94=AF=E6=8C=81=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- impl/hocon/pom.xml | 66 ++++++++++ .../lib/configuration/EasyConfiguration.java | 31 +++++ .../configuration/hocon/CommentedHOCON.java | 26 ++++ .../hocon/HOCONConfigWrapper.java | 117 ++++++++++++++++++ .../hocon/HOCONFileConfigProvider.java | 103 +++++++++++++++ .../exception/HOCONGetValueException.java | 19 +++ .../hocon/source/StringConfigProvider.java | 23 ++++ .../configuration/hocon/util/HOCONUtils.java | 105 ++++++++++++++++ .../test/easyconfiguration/HOCONTest.java | 23 ++++ .../test/easyconfiguration/config/Config.java | 20 +++ pom.xml | 1 + 11 files changed, 534 insertions(+) create mode 100644 impl/hocon/pom.xml create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/EasyConfiguration.java create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/CommentedHOCON.java create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONConfigWrapper.java create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONFileConfigProvider.java create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/exception/HOCONGetValueException.java create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/source/StringConfigProvider.java create mode 100644 impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/util/HOCONUtils.java create mode 100644 impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/HOCONTest.java create mode 100644 impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/config/Config.java diff --git a/impl/hocon/pom.xml b/impl/hocon/pom.xml new file mode 100644 index 0000000..2257f20 --- /dev/null +++ b/impl/hocon/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + cc.carm.lib + easyconfiguration-parent + 3.6.0 + ../../pom.xml + + + easyconfiguration-hocon + jar + + + ${project.jdk.version} + ${project.jdk.version} + UTF-8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + + + ${project.parent.groupId} + easyconfiguration-core + ${project.parent.version} + compile + + + + ${project.parent.groupId} + easyconfiguration-demo + ${project.parent.version} + test + + + + com.typesafe + config + 1.4.2 + compile + + + \ No newline at end of file diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/EasyConfiguration.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/EasyConfiguration.java new file mode 100644 index 0000000..f0e63ca --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/EasyConfiguration.java @@ -0,0 +1,31 @@ +package cc.carm.lib.configuration; + +import cc.carm.lib.configuration.hocon.HOCONFileConfigProvider; + +import java.io.File; +import java.io.IOException; + +public class EasyConfiguration { + public static HOCONFileConfigProvider from(File file, String source) { + HOCONFileConfigProvider provider = new HOCONFileConfigProvider(file); + try { + provider.initializeFile(source); + provider.initializeConfig(); + } catch (IOException e) { + e.printStackTrace(); + } + return provider; + } + + public static HOCONFileConfigProvider from(File file) { + return from(file, file.getName()); + } + + public static HOCONFileConfigProvider from(String fileName) { + return from(fileName, fileName); + } + + public static HOCONFileConfigProvider from(String fileName, String source) { + return from(new File(fileName), source); + } +} diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/CommentedHOCON.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/CommentedHOCON.java new file mode 100644 index 0000000..202bb1f --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/CommentedHOCON.java @@ -0,0 +1,26 @@ +package cc.carm.lib.configuration.hocon; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +public interface CommentedHOCON { + default @NotNull Set getKeys() { + return getKeys(null, true); + } + + String serializeValue(@NotNull String key, @NotNull Object value); + + @Contract("null,_ -> !null;!null,_ -> _") + @Nullable Set getKeys(@Nullable String sectionKey, boolean deep); + + @Nullable Object getValue(@NotNull String key); + + @Nullable String getInlineComment(@NotNull String key); + + @Nullable + List getHeaderComments(@Nullable String key); +} diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONConfigWrapper.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONConfigWrapper.java new file mode 100644 index 0000000..5465e23 --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONConfigWrapper.java @@ -0,0 +1,117 @@ +package cc.carm.lib.configuration.hocon; + +import cc.carm.lib.configuration.core.source.ConfigurationWrapper; +import cc.carm.lib.configuration.hocon.util.HOCONUtils; +import com.typesafe.config.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class HOCONConfigWrapper implements ConfigurationWrapper> { + private static final char SEPARATOR = '.'; + protected final Map data; + + public HOCONConfigWrapper(ConfigObject config) { + this.data = new LinkedHashMap<>(); + + config.forEach((key, value) -> { + Config cfg = config.toConfig(); + ConfigValue cv = cfg.getValue(key); + if (cv.valueType() == ConfigValueType.OBJECT) { + HOCONConfigWrapper.this.data.put(key, new HOCONConfigWrapper((ConfigObject) cv)); + } else { + HOCONConfigWrapper.this.data.put(key, value.unwrapped()); + } + }); + } + + @Override + public @NotNull Map getSource() { + return this.data; + } + + @Override + public @NotNull Set getKeys(boolean deep) { + return this.getValues(deep).keySet(); + } + + @Override + public @NotNull Map getValues(boolean deep) { + return HOCONUtils.getKeysFromObject(this, deep, "").stream().collect( + LinkedHashMap::new, + (map, key) -> map.put(key, get(key)), + LinkedHashMap::putAll + ); + } + + @Override + public void set(@NotNull String path, @Nullable Object value) { + if (value instanceof Map) { + //noinspection unchecked + value = new HOCONConfigWrapper(ConfigFactory.parseMap((Map) value).root()); + } + + HOCONConfigWrapper section = HOCONUtils.getObjectOn(this, path, SEPARATOR); + String simplePath = HOCONUtils.getSimplePath(path, SEPARATOR); + + if (value == null) { + section.data.remove(simplePath); + } else { + section.setDirect(simplePath, value); + } + } + + /** + * 只能设置当前路径下的内容 + * 避免环回 + * + * @param path 路径 + */ + public void setDirect(@NotNull String path, @Nullable Object value) { + this.data.put(path, value); + } + + @Override + public boolean contains(@NotNull String path) { + return this.get(path) != null; + } + + @Override + public @Nullable Object get(@NotNull String path) { + HOCONConfigWrapper section = HOCONUtils.getObjectOn(this, path, SEPARATOR); + return section.getDirect(HOCONUtils.getSimplePath(path, SEPARATOR)); + } + + /** + * 只能获取当前路径下的内容 + * 避免环回 + * + * @param path 路径 + */ + public Object getDirect(@NotNull String path) { + return this.data.get(path); + } + + @Override + public boolean isList(@NotNull String path) { + return this.get(path) instanceof List; + } + + @Override + public @Nullable List getList(@NotNull String path) { + Object val = this.get(path); + return (val instanceof List) ? (List) val : null; + } + + @Override + public boolean isConfigurationSection(@NotNull String path) { + return this.get(path) instanceof HOCONConfigWrapper; + } + + @Override + public @Nullable ConfigurationWrapper> getConfigurationSection(@NotNull String path) { + Object val = get(path); + return (val instanceof HOCONConfigWrapper) ? (HOCONConfigWrapper) val : null; + } +} diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONFileConfigProvider.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONFileConfigProvider.java new file mode 100644 index 0000000..c944754 --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/HOCONFileConfigProvider.java @@ -0,0 +1,103 @@ +package cc.carm.lib.configuration.hocon; + +import cc.carm.lib.configuration.core.ConfigInitializer; +import cc.carm.lib.configuration.core.source.ConfigurationComments; +import cc.carm.lib.configuration.core.source.impl.FileConfigProvider; +import cc.carm.lib.configuration.hocon.exception.HOCONGetValueException; +import cc.carm.lib.configuration.hocon.util.HOCONUtils; +import com.typesafe.config.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.Set; + +public class HOCONFileConfigProvider extends FileConfigProvider implements CommentedHOCON { + protected final @NotNull ConfigurationComments comments = new ConfigurationComments(); + protected HOCONConfigWrapper configuration; + protected ConfigInitializer initializer; + + public HOCONFileConfigProvider(@NotNull File file) { + super(file); + this.initializer = new ConfigInitializer<>(this); + } + + public void initializeConfig() { + try { + this.configuration = new HOCONConfigWrapper(ConfigFactory.parseFile(this.file, ConfigParseOptions.defaults() + .setSyntax(ConfigSyntax.CONF) + .setAllowMissing(false)).root()); + } catch (ConfigException e) { + e.printStackTrace(); + } + } + + @Override + public @NotNull HOCONConfigWrapper getConfiguration() { + return this.configuration; + } + + @Override + public void save() throws IOException { + Files.write(this.file.toPath(), HOCONUtils.renderWithComment(configuration, comments::getHeaderComment).getBytes(StandardCharsets.UTF_8)); + } + + @Override + protected void onReload() throws ConfigException { + this.configuration = new HOCONConfigWrapper(ConfigFactory.parseFile(this.file, ConfigParseOptions.defaults() + .setSyntax(ConfigSyntax.CONF) + .setAllowMissing(false)).root()); + } + + @Override + public @NotNull ConfigurationComments getComments() { + return this.comments; + } + + @Override + public @NotNull ConfigInitializer getInitializer() { + return this.initializer; + } + + @Override + public String serializeValue(@NotNull String key, @NotNull Object value) { + // 带有 key=value 的新空对象 + return ConfigFactory.empty() + .withValue(key, ConfigValueFactory.fromAnyRef(value)) + .root().render(); + } + + @Override + public @Nullable Set getKeys(@Nullable String sectionKey, boolean deep) { + if (sectionKey == null) { // 当前路径 + return HOCONUtils.getKeysFromObject(this.configuration, deep, ""); + } + + HOCONConfigWrapper section; + try { + // 获取目标字段所在路径 + section = (HOCONConfigWrapper) this.configuration.get(sectionKey); + } catch (ClassCastException e) { + // 值和类型不匹配 + throw new HOCONGetValueException(e); + } + if (section == null) { + return null; + } + return HOCONUtils.getKeysFromObject(section, deep, ""); + } + + @Override + public @Nullable Object getValue(@NotNull String key) { + return this.configuration.get(key); + } + + @Override + public @Nullable List getHeaderComments(@Nullable String key) { + return this.comments.getHeaderComment(key); + } +} diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/exception/HOCONGetValueException.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/exception/HOCONGetValueException.java new file mode 100644 index 0000000..ea308a1 --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/exception/HOCONGetValueException.java @@ -0,0 +1,19 @@ +package cc.carm.lib.configuration.hocon.exception; + +public class HOCONGetValueException extends RuntimeException { + public HOCONGetValueException() { + super(); + } + + public HOCONGetValueException(String message) { + super(message); + } + + public HOCONGetValueException(String message, Throwable cause) { + super(message, cause); + } + + public HOCONGetValueException(Throwable cause) { + super(cause); + } +} diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/source/StringConfigProvider.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/source/StringConfigProvider.java new file mode 100644 index 0000000..a4f03dc --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/source/StringConfigProvider.java @@ -0,0 +1,23 @@ +package cc.carm.lib.configuration.hocon.source; + +import cc.carm.lib.configuration.core.source.ConfigurationProvider; +import cc.carm.lib.configuration.core.source.ConfigurationWrapper; +import org.jetbrains.annotations.NotNull; + +/** + * 暂时未实现,原因是如果要实现,就需要修改部分代码 + * @see ConfigurationProvider#save() + * @see ConfigurationProvider#reload() + * 等一系列代码 + */ +public abstract class StringConfigProvider> extends ConfigurationProvider { + protected final @NotNull String source; + + protected StringConfigProvider(@NotNull String source) { + this.source = source; + } + + public @NotNull String getSource() { + return this.source; + } +} diff --git a/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/util/HOCONUtils.java b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/util/HOCONUtils.java new file mode 100644 index 0000000..29f9bd6 --- /dev/null +++ b/impl/hocon/src/main/java/cc/carm/lib/configuration/hocon/util/HOCONUtils.java @@ -0,0 +1,105 @@ +package cc.carm.lib.configuration.hocon.util; + +import cc.carm.lib.configuration.hocon.HOCONConfigWrapper; +import cc.carm.lib.configuration.hocon.exception.HOCONGetValueException; +import com.typesafe.config.*; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public abstract class HOCONUtils { + public static String getSimplePath(String path, char separator) { + int index = path.lastIndexOf(separator); + return (index == -1) ? path : path.substring(index + 1); + } + + public static HOCONConfigWrapper getObjectOn(@NotNull HOCONConfigWrapper parent, @NotNull String path, char separator) { + String currentPath = path; + HOCONConfigWrapper currentObject = parent; + int index; + while ((index = currentPath.indexOf(separator)) != -1) { + HOCONConfigWrapper previousObject = currentObject; + String pathName = currentPath.substring(0, index); + try { + currentObject = (HOCONConfigWrapper) previousObject.getDirect(pathName); + } catch (ClassCastException e) { + throw new HOCONGetValueException(e); + } + if (currentObject == null) { + currentObject = new HOCONConfigWrapper(ConfigFactory.empty().root()); + previousObject.setDirect(pathName, currentObject); + } + currentPath = currentPath.substring(0, index); + } + + return currentObject; + } + + /** + * 在 Object 中获取所有键 + * 思路:在第一次执行时 prefix 应该是 "" + * 后续找到了更深层的键,将会变为 "deep." + * 下一次键名就是 "deep.key" + * + * @param parent Object + * @param deep 是否更深层获取 + * @param prefix 当前 Object 键名前缀 + * @return Object 中的所有键 + */ + public static LinkedHashSet getKeysFromObject(HOCONConfigWrapper parent, boolean deep, String prefix) { + return parent.getSource().entrySet().stream().collect( + LinkedHashSet::new, + (set, entry) -> { + Object value = entry.getValue(); + if (value instanceof HOCONConfigWrapper && deep) { + set.addAll(HOCONUtils.getKeysFromObject((HOCONConfigWrapper) value, true, prefix + entry.getKey() + ".")); + } else { + set.add(prefix + entry.getKey()); + } + }, + LinkedHashSet::addAll + ); + } + + /** + * 将 Object 保存为字符串 + * 并使用注释器打上注释 + * + * @param object Object + * @param commenter 注释器 + * @return 保存的字符串 + */ + public static @NotNull String renderWithComment(@NotNull HOCONConfigWrapper object, @NotNull Function> commenter) { + return HOCONUtils.makeConfigWithComment(object, "", commenter).root().render( + ConfigRenderOptions.defaults() + .setJson(false) + .setOriginComments(false) + ); + } + + public static @NotNull Config makeConfigWithComment(@NotNull HOCONConfigWrapper object, @NotNull String prefix, @NotNull Function> commenter) { + Config config = ConfigFactory.empty(); + for (Map.Entry entry : object.getSource().entrySet()) { + String key = entry.getKey(); + String fullKey = prefix + key; + Object value = entry.getValue(); + ConfigValue result; + if (value instanceof Iterable) { + result = ConfigValueFactory.fromIterable((Iterable) value); + } else if (value instanceof HOCONConfigWrapper) { + result = makeConfigWithComment((HOCONConfigWrapper) value, fullKey + ".", commenter).root(); + } else { + result = ConfigValueFactory.fromAnyRef(value); + } + result = result.withOrigin( + ConfigOriginFactory.newSimple() + .withComments(commenter.apply(fullKey)) + ); + config = config.withValue(key, result); + } + return config; + } +} diff --git a/impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/HOCONTest.java b/impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/HOCONTest.java new file mode 100644 index 0000000..82ccb3b --- /dev/null +++ b/impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/HOCONTest.java @@ -0,0 +1,23 @@ +package online.flowerinsnow.test.easyconfiguration; + +import cc.carm.lib.configuration.EasyConfiguration; +import cc.carm.lib.configuration.hocon.HOCONFileConfigProvider; +import online.flowerinsnow.test.easyconfiguration.config.Config; +import org.junit.Test; + +import java.io.File; + +public class HOCONTest { + @Test + public void onTest() { + HOCONFileConfigProvider provider = EasyConfiguration.from(new File("target/hocon.conf")); + provider.initialize(Config.class); + try { + provider.reload(); + } catch (Exception e) { + e.printStackTrace(); + } + + System.out.println("Config.TestObject.TEST_BOOLEAN.getNotNull() = " + Config.TestObject.TEST_BOOLEAN.getNotNull()); + } +} diff --git a/impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/config/Config.java b/impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/config/Config.java new file mode 100644 index 0000000..8fb56a0 --- /dev/null +++ b/impl/hocon/src/test/java/online/flowerinsnow/test/easyconfiguration/config/Config.java @@ -0,0 +1,20 @@ +package online.flowerinsnow.test.easyconfiguration.config; + +import cc.carm.lib.configuration.core.ConfigurationRoot; +import cc.carm.lib.configuration.core.annotation.HeaderComment; +import cc.carm.lib.configuration.core.value.type.ConfiguredList; +import cc.carm.lib.configuration.core.value.type.ConfiguredValue; + +public class Config extends ConfigurationRoot { + @HeaderComment("测试字段 int") + public static final ConfiguredValue TEST_INT = ConfiguredValue.of(Integer.class, 15); + + @HeaderComment("测试字段 List") + public static final ConfiguredList TEST_LIST_STRING = ConfiguredList.of(String.class, "li", "li", "li1"); + + @HeaderComment("测试对象") + public static class TestObject extends ConfigurationRoot { + @HeaderComment("测试字段 Boolean") + public static final ConfiguredValue TEST_BOOLEAN = ConfiguredValue.of(Boolean.class, true); + } +} diff --git a/pom.xml b/pom.xml index 8ab6985..5c7a404 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ impl/yaml impl/json impl/sql + impl/hocon EasyConfiguration