红云cloud
## 在 bukkit 配置框架下读取并保存配置文件

### 新版本的操作

自 https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/commits/3e2dd2bc120754ea4db193e878050d0eb31a6894
之后的版本,bukkit 添加了注释相关的支持。如您的插件只服务于新版本,您可以跳过此篇。

### 旧版本的方法
SnakeYaml 自 1.28 开始添加了对 comments 的解析。在 Node 实例中可通过读取 `inLineComments` `blockComments` 与 `endComments` 来
存储注释。可通过以下代码启用

```java// 启用读取注释
loaderOptions.setProcessComments(true);

// 启用保存注释
dumperOptions.setProcessComments(true);
复制代码```

随后可以实现 `bukkit` 中的 `FileConfiguration` 类,在 `loadFromString` 中将 `Node` 中的 `comments` 保存,
并在 `saveToString` 时还原。

### 存在的问题

#### 保存序列时注释与习惯的位置不一致

```yaml
# AAA
id: 101
# BBB
test:
# ccc
- 0复制代码
```

```yaml
# AAA
id: 101
# BBB
test:
- # ccc
  0
复制代码```

### 实现
本代码以 WTFPL 授权,请随意修改并使用

```java

/*
*         DO WHAT THE ** YOU WANT TO PUBLIC LICENSE
*                     Version 2, December 2004
*
*  Copyright (C) 2004 HongYunCloud
*
*  Everyone is permitted to copy and distribute verbatim or modified
*  copies of this license document, and changing it is allowed as long
*  as the name is changed.
*
*             DO WHAT THE ** YOU WANT TO PUBLIC LICENSE
*    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
*   0. You just DO WHAT THE ** YOU WANT TO.
*/
package io.github.hongyuncloud.config;

import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
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.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.YAMLException;
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.logging.Level;
import java.util.logging.Logger;

// HongYunCloud Yaml Configuration
// version 1.0-SNAPSHOT
//
// required snakeyaml 1.28+
// it provide on 1.13+, so you need shadow yourself version before 1.13
// implementation("org.yaml:snakeyaml:2.0")
public final class HcYamlConfiguration extends FileConfiguration {
  private static final /* @Nonnull */ Logger logger = Logger.getLogger(HcYamlConfiguration.class.getName());
  private final /* @Nonnull */ HcYamlConstructor constructor;
  private final /* @Nonnull */ HcYamlRepresenter representer;
  private final /* @Nonnull */ Yaml yaml;
  private final /* @Nonnull */ Map commentMap;

  public HcYamlConfiguration() {
    this(null);
  }

  public HcYamlConfiguration(final /* @Nullable */ Configuration defaults) {
    super(defaults);
    final DumperOptions dumperOptions = new DumperOptions();
    dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
    dumperOptions.setProcessComments(true);

    final LoaderOptions loaderOptions = new LoaderOptions();
    loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE);
    loaderOptions.setCodePointLimit(Integer.MAX_VALUE);
    loaderOptions.setProcessComments(true);

    constructor = new HcYamlConstructor(loaderOptions);

    representer = new HcYamlRepresenter(dumperOptions);
    representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);

    yaml = new Yaml(constructor, representer, dumperOptions, loaderOptions);
    commentMap = new LinkedHashMap
  }

  public static /* @Nonnull */ HcYamlConfiguration loadConfiguration(final /* @Nonnull */  File file) {
    final HcYamlConfiguration config = new HcYamlConfiguration();
    try {
      config.load(file);
    } catch (final FileNotFoundException e) {
      // ignored
    } catch (final IOException | InvalidConfigurationException e) {
      logger.log(Level.SEVERE, "Cannot load " + file, e);
    }
    return config;
  }

  public static /* @Nonnull */ HcYamlConfiguration loadConfiguration(final /* @Nonnull */ Reader reader) {
    final HcYamlConfiguration config = new HcYamlConfiguration();
    try {
      config.load(reader);
    } catch (final IOException | InvalidConfigurationException e) {
      logger.log(Level.SEVERE, "Cannot load configuration from stream", e);
    }
    return config;
  }

  @Override
  public void loadFromString(final /* @Nonnull */ String contents) throws InvalidConfigurationException {
    final MappingNode node;
    try (final Reader reader = new UnicodeReader(new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)))) {
      final Node rawNode = yaml.compose(reader);
      try {
        node = (MappingNode) rawNode;
      } catch (final ClassCastException e) {
        throw new InvalidConfigurationException("Top level is not a Map.");
      }
    } catch (final YAMLException | IOException | ClassCastException e) {
      throw new InvalidConfigurationException(e);
    }

    this.map.clear();
    this.commentMap.clear();

    if (node != null) {
      fromNodeTree(node, this);
    }
  }

  /* [url=home.php?mod=space&uid=520054]@override[/url] */
  protected /* @Nonnull */ String buildHeader() {
    return "";
  }

  @Override
  public /* @Nonnull */ String saveToString() {
    try(final StringWriter writer = new StringWriter()){
      final MappingNode node = toNodeTree(this);
      if (node.getValue().isEmpty()) {
        node.setFlowStyle(DumperOptions.FlowStyle.FLOW);
      }
      yaml.serialize(node, writer);
      return writer.toString();
    }catch (final IOException e){
      throw new UncheckedIOException(e);
    }
  }

  private void fromNodeTree(final /* @Nonnull */ MappingNode input, final /* @Nonnull */ ConfigurationSection section) {
    constructor.flattenMapping(input);
    commentMap.put(section.getCurrentPath() + options().pathSeparator() + "v", createCommentStorage(input));
    for (final NodeTuple nodeTuple : input.getValue()) {
      final Node key = nodeTuple.getKeyNode();
      final String keyString = String.valueOf(constructor.construct(key));
      final String path = section.getCurrentPath() + options().pathSeparator() + keyString;
      commentMap.put(path + options().pathSeparator() + "k", createCommentStorage(key));

      Node value = nodeTuple.getValueNode();

      while (value instanceof AnchorNode) {
        value = ((AnchorNode) value).getRealNode();
      }

      if (value instanceof MappingNode && !hasSerializedTypeKey((MappingNode) value)) {
        fromNodeTree((MappingNode) value, section.createSection(keyString));
      }else {
        final String valuePath = path + options().pathSeparator() + "v";
        section.set(keyString, constructor.construct(value));
        if (value instanceof SequenceNode) {
          final SequenceNode sequenceValue = (SequenceNode) value;
          for (int i = 0; i
            commentMap.put(
                valuePath + options().pathSeparator() + i,
                createCommentStorage(sequenceValue.getValue().get(i))
            );
          }
        }
        commentMap.put(valuePath, createCommentStorage(value));
      }
    }
  }

  private boolean hasSerializedTypeKey(final /* @Nonnull */ MappingNode node) {
    for (final NodeTuple nodeTuple : node.getValue()) {
      final Node keyNode = nodeTuple.getKeyNode();
      if (!(keyNode instanceof ScalarNode)) continue;
      final String key = ((ScalarNode) keyNode).getValue();
      if (key.equals(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
        return true;
      }
    }
    return false;
  }

  private /* @Nonnull */ MappingNode toNodeTree(final /* @Nonnull */ ConfigurationSection section) {
    final List nodeTuples = new ArrayList
    for (final Map.Entry entry : section.getValues(false).entrySet()) {
      final Node key = representer.represent(entry.getKey());
      final String path = section.getCurrentPath() + options().pathSeparator() + entry.getKey();
      applyCommentStorage(key, commentMap.get(path + options.pathSeparator() + "k"));

      final Node value;
      if (entry.getValue() instanceof ConfigurationSection) {
        value = toNodeTree((ConfigurationSection) entry.getValue());
      } else {
        final String valuePath = path + options().pathSeparator() + "v";
        value = representer.represent(entry.getValue());
        applyCommentStorage(value, commentMap.get(valuePath));
        if(value instanceof SequenceNode) {
          final List sequenceValue = ((SequenceNode) value).getValue();
          for (int i = 0; i
            applyCommentStorage(sequenceValue.get(i), commentMap.get(valuePath + options().pathSeparator() + i));
          }
        }
      }
      nodeTuples.add(new NodeTuple(key, value));
    }

    final MappingNode result = new MappingNode(Tag.MAP, nodeTuples, DumperOptions.FlowStyle.BLOCK);
    applyCommentStorage(result, commentMap.get(section.getCurrentPath() + options().pathSeparator() + "v"));
    return result;
  }

  private /* @Nonnull */ HcCommentStorage createCommentStorage(final /* @Nullable */ Node node) {
    if(node == null){
      return new HcCommentStorage(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
    }
    return new HcCommentStorage(node.getInLineComments(), node.getBlockComments(), node.getEndComments());
  }

  private void applyCommentStorage(final /* @Nullable */ Node node, final /* @Nullable */ HcCommentStorage storage) {
    if (node == null || storage == null) {
      return;
    }
    node.setInLineComments(storage.inLineComments);
    node.setBlockComments(storage.blockComments);
    node.setEndComments(storage.endComments);
  }

  private static final class HcCommentStorage {
    private final /* @Nonnull */ List inLineComments;
    private final /* @Nonnull */ List blockComments;
    private final /* @Nonnull */ List endComments;


    private HcCommentStorage(
        final /* @Nonnull */ List inLineComments,
        final /* @Nonnull */ List blockComments,
        final /* @Nonnull */ List endComments
    ) {
      this.inLineComments = inLineComments == null ? Collections.emptyList() : new ArrayList
      this.blockComments = blockComments == null ? Collections.emptyList() : new ArrayList
      this.endComments = endComments == null ? Collections.emptyList() : new ArrayList
    }
  }

  private static final class HcYamlConstructor extends SafeConstructor {
    public HcYamlConstructor(final /* @Nonnull */ LoaderOptions loaderOptions) {
      super(loaderOptions);
      this.yamlConstructors.put(Tag.MAP, new HcYamlConstructor.ConstructCustomObject());
    }

    @Override
    public void flattenMapping(final /* @Nonnull */ MappingNode node) {
      super.flattenMapping(node);
    }

    public /* @Nonnull */ Object construct(final /* @Nonnull */ Node node) {
      return constructObject(node);
    }

    private class ConstructCustomObject extends ConstructYamlMap {
      @Override
      public final /* @Nonnull */ Object construct(final /* @Nonnull */ Node node) {
        if (node.isTwoStepsConstruction()) {
          throw new YAMLException("Unexpected referential mapping structure. Node: " + node);
        }

        Map raw = (Map) super.construct(node);

        if (raw.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
          Map typed = new LinkedHashMap
          for (Map.Entry entry : raw.entrySet()) {
            typed.put(entry.getKey().toString(), entry.getValue());
          }

          try {
            return ConfigurationSerialization.deserializeObject(typed);
          } catch (IllegalArgumentException ex) {
            throw new YAMLException("Could not deserialize object", ex);
          }
        }

        return raw;
      }

      @Override
      public void construct2ndStep(final /* @Nonnull */ Node node, final /* @Nonnull */ Object object) {
        throw new YAMLException("Unexpected referential mapping structure. Node: " + node);
      }
    }
  }

  private static final class HcYamlRepresenter extends Representer {
    public HcYamlRepresenter(final /* @Nonnull */ DumperOptions options) {
      super(options);
      this.multiRepresenters.put(ConfigurationSection.class, new RepresentConfigurationSection());
      this.multiRepresenters.put(ConfigurationSerializable.class, new RepresentConfigurationSerializable());
      this.multiRepresenters.remove(Enum.class);
    }

    // SPIGOT-6949: Used by configuration sections that are nested within lists or maps.
    private final class RepresentConfigurationSection extends RepresentMap {
      @Override
      public /* @Nonnull */ Node representData(final /* @Nonnull */ Object data) {
        return super.representData(((ConfigurationSection) data).getValues(false));
      }
    }

    private final class RepresentConfigurationSerializable extends RepresentMap {
      @Override
      public /* @Nonnull */ Node representData(final /* @Nonnull */ Object data) {
        final ConfigurationSerializable serializable = (ConfigurationSerializable) data;
        final Map values = new LinkedHashMap
        values.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(serializable.getClass()));
        values.putAll(serializable.serialize());
        return super.representData(values);
      }
    }
  }
}
复制代码
```

第一页 上一页 下一页 最后一页