From 18199cd1cfb30bcfd15c459b6f9082b2b58d4d8e Mon Sep 17 00:00:00 2001 From: Carm Date: Thu, 13 Feb 2025 07:12:55 +0800 Subject: [PATCH] feat(yaml): Try implement yaml comments --- core/pom.xml | 2 +- demo/pom.xml | 2 +- features/commentable/pom.xml | 2 +- features/file/pom.xml | 2 +- features/section/pom.xml | 2 +- features/versioned/pom.xml | 2 +- pom.xml | 2 +- providers/gson/pom.xml | 2 +- .../source/json/JSONConfigFactory.java | 3 +- providers/yaml/pom.xml | 4 +- .../source/yaml/YAMLConfigFactory.java | 82 ++++++- .../source/yaml/YAMLOptions.java | 27 +++ .../configuration/source/yaml/YAMLSource.java | 220 +++++++++++++++++- 13 files changed, 339 insertions(+), 13 deletions(-) create mode 100644 providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java diff --git a/core/pom.xml b/core/pom.xml index 5f9a61e..aa4ad15 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ easyconfiguration-parent cc.carm.lib - 3.9.1 + 4.0.0 4.0.0 diff --git a/demo/pom.xml b/demo/pom.xml index 13700da..b2a1fc9 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -5,7 +5,7 @@ easyconfiguration-parent cc.carm.lib - 3.9.1 + 4.0.0 4.0.0 diff --git a/features/commentable/pom.xml b/features/commentable/pom.xml index 5aee317..6b056ea 100644 --- a/features/commentable/pom.xml +++ b/features/commentable/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib easyconfiguration-parent - 3.9.1 + 4.0.0 ../../pom.xml diff --git a/features/file/pom.xml b/features/file/pom.xml index 861bcb1..1ebb3f8 100644 --- a/features/file/pom.xml +++ b/features/file/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib easyconfiguration-parent - 3.9.1 + 4.0.0 ../../pom.xml diff --git a/features/section/pom.xml b/features/section/pom.xml index 9a11655..192790c 100644 --- a/features/section/pom.xml +++ b/features/section/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib easyconfiguration-parent - 3.9.1 + 4.0.0 ../../pom.xml diff --git a/features/versioned/pom.xml b/features/versioned/pom.xml index 1b40382..3c6f09d 100644 --- a/features/versioned/pom.xml +++ b/features/versioned/pom.xml @@ -6,7 +6,7 @@ cc.carm.lib easyconfiguration-parent - 3.9.1 + 4.0.0 ../../pom.xml diff --git a/pom.xml b/pom.xml index a09011e..05bae52 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ cc.carm.lib easyconfiguration-parent pom - 3.9.1 + 4.0.0 core features/section diff --git a/providers/gson/pom.xml b/providers/gson/pom.xml index 0732fe1..405f0fd 100644 --- a/providers/gson/pom.xml +++ b/providers/gson/pom.xml @@ -5,7 +5,7 @@ easyconfiguration-parent cc.carm.lib - 3.9.1 + 4.0.0 ../../pom.xml 4.0.0 diff --git a/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java b/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java index f286c01..c4e488b 100644 --- a/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java +++ b/providers/gson/src/main/java/cc/carm/lib/configuration/source/json/JSONConfigFactory.java @@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder; import org.jetbrains.annotations.NotNull; import java.io.File; +import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; @@ -57,7 +58,7 @@ public class JSONConfigFactory extends FileConfigFactory(this.adapters, this.options, new ConcurrentHashMap<>(), this.initializer) { + return new ConfigurationHolder(this.adapters, this.options, new HashMap<>(), this.initializer) { final JSONSource source = new JSONSource(this, configFile, sourcePath, gson); @Override diff --git a/providers/yaml/pom.xml b/providers/yaml/pom.xml index f1bd5ed..3e1239f 100644 --- a/providers/yaml/pom.xml +++ b/providers/yaml/pom.xml @@ -2,13 +2,13 @@ + 4.0.0 easyconfiguration-parent cc.carm.lib - 3.9.1 + 4.0.0 ../../pom.xml - 4.0.0 ${project.jdk.version} ${project.jdk.version} diff --git a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java index 5458631..5d063aa 100644 --- a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java +++ b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLConfigFactory.java @@ -1,4 +1,84 @@ package cc.carm.lib.configuration.source.yaml; -public class YAMLConfigFactory { +import cc.carm.lib.configuration.commentable.CommentableMetaTypes; +import cc.carm.lib.configuration.source.ConfigurationHolder; +import cc.carm.lib.configuration.source.file.FileConfigFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; + +import java.io.File; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public class YAMLConfigFactory extends FileConfigFactory, YAMLConfigFactory> { + + public static YAMLConfigFactory from(@NotNull String path) { + return new YAMLConfigFactory(new File(path)); + } + + public static YAMLConfigFactory from(@NotNull File file) { + return new YAMLConfigFactory(file); + } + + public static YAMLConfigFactory from(@NotNull File parent, @NotNull String configName) { + return new YAMLConfigFactory(new File(parent, configName)); + } + + public YAMLConfigFactory(@NotNull File file) { + super(file); + } + + @Override + protected YAMLConfigFactory self() { + return this; + } + + public YAMLConfigFactory loader(@NotNull LoaderOptions loaderOptions) { + return option(YAMLOptions.LOADER, loaderOptions); + } + + public YAMLConfigFactory loader(@NotNull Consumer modifier) { + return option(YAMLOptions.LOADER, modifier); + } + + public YAMLConfigFactory indent(@Range(from = 2, to = 8) int indent) { + return dumper(d -> d.setIndent(indent)); + } + + public YAMLConfigFactory width(@Range(from = 8, to = 1000) int width) { + return dumper(d -> d.setWidth(width)); + } + + public YAMLConfigFactory dumper(@NotNull DumperOptions dumperOptions) { + return option(YAMLOptions.DUMPER, dumperOptions); + } + + public YAMLConfigFactory dumper(@NotNull Consumer modifier) { + return option(YAMLOptions.DUMPER, modifier); + } + + @Override + public @NotNull ConfigurationHolder build() { + + File configFile = this.file; + String sourcePath = this.resourcePath; + + CommentableMetaTypes.register(this.initializer); // Register commentable meta types + + return new ConfigurationHolder( + this.adapters, this.options, new HashMap<>(), this.initializer + ) { + final @NotNull YAMLSource source = new YAMLSource(this, configFile, sourcePath); + + @Override + public @NotNull YAMLSource config() { + return this.source; + } + }; + } + + } diff --git a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java new file mode 100644 index 0000000..e061a9e --- /dev/null +++ b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLOptions.java @@ -0,0 +1,27 @@ +package cc.carm.lib.configuration.source.yaml; + +import cc.carm.lib.configuration.source.option.ConfigurationOption; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; + +public interface YAMLOptions { + + + ConfigurationOption LOADER = ConfigurationOption.of(() -> { + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setMaxAliasesForCollections(100); // 100 aliases + loaderOptions.setCodePointLimit(5 * 1024 * 1024); // 5MB + return loaderOptions; + }); + + ConfigurationOption DUMPER = ConfigurationOption.of(() -> { + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setWidth(120); + options.setProcessComments(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return options; + }); + + +} diff --git a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java index d7af585..054062d 100644 --- a/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java +++ b/providers/yaml/src/main/java/cc/carm/lib/configuration/source/yaml/YAMLSource.java @@ -1,4 +1,222 @@ package cc.carm.lib.configuration.source.yaml; -public class YAMLSource { +import cc.carm.lib.configuration.commentable.CommentableMetaTypes; +import cc.carm.lib.configuration.source.ConfigurationHolder; +import cc.carm.lib.configuration.source.file.FileConfigSource; +import cc.carm.lib.configuration.source.meta.ConfigurationMetadata; +import cc.carm.lib.configuration.source.section.ConfigureSection; +import cc.carm.lib.configuration.source.section.MemorySection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.*; +import org.yaml.snakeyaml.reader.UnicodeReader; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +public class YAMLSource extends FileConfigSource, YAMLSource> { + + protected final @NotNull YamlConstructor yamlConstructor; + protected final @NotNull YamlRepresenter yamlRepresenter; + protected final @NotNull Yaml yaml; + + protected @Nullable MemorySection rootSection; + + protected YAMLSource(@NotNull ConfigurationHolder holder, + @NotNull File file, @Nullable String resourcePath) { + super(holder, 0, file, resourcePath); + this.yamlConstructor = new YamlConstructor(loaderOptions()); + this.yamlRepresenter = new YamlRepresenter(dumperOptions()); + this.yaml = new Yaml(this.yamlConstructor, this.yamlRepresenter, dumperOptions()); + + initialize(); + } + + public void initialize() { + try { + initializeFile(); + onReload(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + protected YAMLSource self() { + return null; + } + + @Override + public @NotNull Map original() { + return section().data(); + } + + @Override + public @NotNull MemorySection section() { + return Objects.requireNonNull(this.rootSection, "Root section is not initialized."); + } + + public @NotNull LoaderOptions loaderOptions() { + return holder().options().get(YAMLOptions.LOADER); + } + + public @NotNull DumperOptions dumperOptions() { + return holder().options().get(YAMLOptions.DUMPER); + } + + @NotNull + private MappingNode toNodeTree(@NotNull final ConfigureSection section) { + List nodeTuples = new ArrayList<>(); + for (final Map.Entry entry : section.getValues(false).entrySet()) { + + final Node keyNode = this.yaml.represent(entry.getKey()); + final Node valueNode; + if (entry.getValue() instanceof ConfigureSection) { + valueNode = this.toNodeTree((ConfigureSection) entry.getValue()); + } else { + valueNode = this.yaml.represent(entry.getValue()); + } + + keyNode.setBlockComments(buildComments(CommentType.BLOCK, CommentableMetaTypes.HEADER_COMMENTS, entry.getKey())); + if (valueNode instanceof MappingNode || valueNode instanceof SequenceNode) { + keyNode.setInLineComments(buildComment(CommentType.IN_LINE, CommentableMetaTypes.INLINE_COMMENT, entry.getKey())); + } else { + valueNode.setInLineComments(buildComment(CommentType.IN_LINE, CommentableMetaTypes.INLINE_COMMENT, entry.getKey())); + } +// keyNode.setEndComments(buildComments(CommentType.BLOCK, CommentableMetaTypes.FOOTER_COMMENTS, entry.getKey())); + + nodeTuples.add(new NodeTuple(keyNode, valueNode)); + } + + return new MappingNode(Tag.MAP, nodeTuples, DumperOptions.FlowStyle.BLOCK); + } + + public List buildComments(@NotNull CommentType type, @NotNull ConfigurationMetadata> meta, + @Nullable String path) { + List comments = holder.metadata(path).get(meta); + if (comments == null) return Collections.emptyList(); + return comments.stream().map(s -> { + if (s.isEmpty()) return new CommentLine(null, null, "", CommentType.BLANK_LINE); + else return new CommentLine(null, null, s, type); + }).collect(Collectors.toList()); + } + + public List buildComment(@NotNull CommentType type, @NotNull ConfigurationMetadata meta, + @Nullable String path) { + String comment = holder.metadata(path).get(meta); + if (comment == null || comment.isEmpty()) return Collections.emptyList(); + return Collections.singletonList(new CommentLine(null, null, comment, type)); + } + + + @NotNull + public String saveToString() { + + MappingNode mappingNode = this.toNodeTree(this); +// mappingNode.setBlockComments(this.getCommentLines(this.saveHeader(this.getOptions().getHeader()), CommentType.BLOCK)); +// mappingNode.setEndComments(this.getCommentLines(this.getOptions().getFooter(), CommentType.BLOCK)); + + StringWriter writer = new StringWriter(); + if ((mappingNode.getBlockComments() == null || mappingNode.getBlockComments().isEmpty()) + && (mappingNode.getEndComments() == null || mappingNode.getEndComments().isEmpty()) + && (mappingNode.getInLineComments() == null || mappingNode.getInLineComments().isEmpty()) + && mappingNode.getValue().isEmpty()) { + writer.write(""); + } else { + if (mappingNode.getValue().isEmpty()) { + mappingNode.setFlowStyle(DumperOptions.FlowStyle.FLOW); + } + this.yaml.serialize(mappingNode, writer); + } + + return writer.toString(); + } + + @Override + public void save() throws Exception { + fileWriter(w -> w.write(saveToString())); + } + + @Override + protected void onReload() throws Exception { + this.rootSection = fileReadString(this::loadFromString); + } + + public @NotNull MemorySection loadFromString(@NotNull String data) throws Exception { + MappingNode mappingNode; + try (Reader reader = new UnicodeReader(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)))) { + Node rawNode = this.yaml.compose(reader); + mappingNode = (MappingNode) rawNode; + } + if (mappingNode == null) return MemorySection.root(this); + + Map map = new LinkedHashMap<>(); + this.constructMap(mappingNode, map); + return MemorySection.root(this, map); + } + + private void constructMap(@NotNull MappingNode mappingNode, @NotNull Map section) { + this.yamlConstructor.flattenMapping(mappingNode); + for (final NodeTuple nodeTuple : mappingNode.getValue()) { + + final Node keyNode = nodeTuple.getKeyNode(); + final String key = String.valueOf(this.yamlConstructor.construct(keyNode)); + Node valueNode = nodeTuple.getValueNode(); + + while (valueNode instanceof AnchorNode) { + valueNode = ((AnchorNode) valueNode).getRealNode(); + } + + if (valueNode instanceof MappingNode) { + Map child = new LinkedHashMap<>(); + this.constructMap((MappingNode) valueNode, child); + section.put(key, child); + } else { + section.put(key, this.yamlConstructor.construct(valueNode)); + } + } + } + + public static class YamlRepresenter extends Representer { + + public YamlRepresenter(@NotNull final DumperOptions dumperOptions) { + super(dumperOptions); + this.multiRepresenters.put(ConfigureSection.class, new RepresentMap() { + @NotNull + @Override + public Node representData(@NotNull final Object object) { + return super.representData(((ConfigureSection) object).getValues(false)); + } + }); + this.multiRepresenters.remove(Enum.class); + } + } + + public static class YamlConstructor extends SafeConstructor { + + public YamlConstructor(@NotNull final LoaderOptions loaderOptions) { + super(loaderOptions); + } + + @Nullable + Object construct(@NotNull final Node node) { + return this.constructObject(node); + } + + @Override + protected void flattenMapping(@NotNull final MappingNode mappingNode) { + super.flattenMapping(mappingNode); + } + } + + }