package net.knarcraft.blacksmith.config; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A YAML configuration which retains all comments * *

This configuration converts all comments to YAML values when loaded, which all start with comment_. When saved, * those YAML values are converted to normal text comments. This ensures that the comments aren't removed by the * YamlConfiguration during its parsing.

* *

Note: When retrieving a configuration section, that will include comments that start with "comment_". You should * filter those before parsing input.

* * @author Kristian Knarvik * @author Thorin */ public class StargateYamlConfiguration extends YamlConfiguration { private static final String START_OF_COMMENT_LINE = "[HASHTAG]"; private static final String END_OF_COMMENT = "_endOfComment_"; private static final String START_OF_COMMENT = "comment_"; @Override @NotNull @SuppressWarnings("deprecation") protected String buildHeader() { return ""; } @Override @NotNull public String saveToString() { // Convert YAML comments to normal comments return this.convertYAMLMappingsToComments(super.saveToString()); } @Override public void loadFromString(@NotNull String contents) throws InvalidConfigurationException { // Convert normal comments to YAML comments to prevent them from disappearing super.loadFromString(this.convertCommentsToYAMLMappings(contents)); } /** * Gets a configuration section's keys, without any comment entries * * @param configurationSection

The configuration section to get keys for

* @param deep

Whether to get keys for child elements as well

* @return

The configuration section's keys, with comment entries removed

*/ public static Set getKeysWithoutComments(@NotNull ConfigurationSection configurationSection, boolean deep) { Set keys = new HashSet<>(configurationSection.getKeys(deep)); keys.removeIf(key -> key.matches(START_OF_COMMENT + "[0-9]+")); return keys; } /** * Reads a file with comments, and recreates them into yaml mappings * *

A mapping follows this format: comment_{CommentNumber}: "The comment" * This needs to be done as comments otherwise get removed using * the {@link FileConfiguration#save(File)} method. The config * needs to be saved if a config value has changed.

*/ @NotNull private String convertCommentsToYAMLMappings(@NotNull String configString) { StringBuilder yamlBuilder = new StringBuilder(); List currentComment = new ArrayList<>(); int commentId = 0; int previousIndentation = 0; for (String line : configString.split("\n")) { String trimmed = line.trim(); if (trimmed.startsWith("#")) { // Store the indentation of the block if (currentComment.isEmpty()) { previousIndentation = getIndentation(line); } //Temporarily store the comment line addComment(currentComment, trimmed); } else { addYamlString(yamlBuilder, currentComment, line, previousIndentation, commentId); commentId++; previousIndentation = 0; } } return yamlBuilder.toString(); } /** * Adds a YAML string to the given string builder * * @param yamlBuilder

The string builder used for building YAML

* @param currentComment

The comment to add as a YAML string

* @param line

The current line

* @param previousIndentation

The indentation of the current block comment

* @param commentId

The id of the comment

*/ private void addYamlString(@NotNull StringBuilder yamlBuilder, @NotNull List currentComment, @NotNull String line, int previousIndentation, int commentId) { String trimmed = line.trim(); //Write the full formatted comment to the StringBuilder if (!currentComment.isEmpty()) { int indentation = trimmed.isEmpty() ? previousIndentation : getIndentation(line); generateCommentYAML(yamlBuilder, currentComment, commentId, indentation); currentComment.clear(); } //Add the non-comment line assuming it isn't empty if (!trimmed.isEmpty()) { yamlBuilder.append(line).append("\n"); } } /** * Adds the given comment to the given list * * @param commentParts

The list to add to

* @param comment

The comment to add

*/ private void addComment(@NotNull List commentParts, @NotNull String comment) { if (comment.startsWith("# ")) { commentParts.add(comment.replaceFirst("# ", START_OF_COMMENT_LINE)); } else { commentParts.add(comment.replaceFirst("#", START_OF_COMMENT_LINE)); } } /** * Generates a YAML-compatible string for one comment block * * @param yamlBuilder

The string builder to add the generated YAML to

* @param commentLines

The lines of the comment to convert into YAML

* @param commentId

The unique id of the comment

* @param indentation

The indentation to add to every line

*/ private void generateCommentYAML(@NotNull StringBuilder yamlBuilder, @NotNull List commentLines, int commentId, int indentation) { String subIndentation = this.addIndentation(indentation + 2); //Add the comment start marker yamlBuilder.append(this.addIndentation(indentation)).append(START_OF_COMMENT).append(commentId).append(": |\n"); for (String commentLine : commentLines) { //Add each comment line with the proper indentation yamlBuilder.append(subIndentation).append(commentLine).append("\n"); } //Add the comment end marker yamlBuilder.append(subIndentation).append(subIndentation).append(END_OF_COMMENT).append("\n"); } /** * Converts the internal YAML mapping format to a readable config file * *

The internal YAML structure is converted to a string with the same format as a standard configuration file. * The internal structure has comments in the format: START_OF_COMMENT + id + multi-line YAML string + * END_OF_COMMENT.

* * @param yamlString

A string using the YAML format

* @return

The corresponding comment string

*/ @NotNull private String convertYAMLMappingsToComments(@NotNull String yamlString) { StringBuilder finalText = new StringBuilder(); String[] lines = yamlString.split("\n"); for (int currentIndex = 0; currentIndex < lines.length; currentIndex++) { String line = lines[currentIndex]; String possibleComment = line.trim(); if (possibleComment.startsWith(START_OF_COMMENT)) { //Add an empty line before every comment block finalText.append("\n"); currentIndex = readComment(finalText, lines, currentIndex + 1, getIndentation(line)); } else { //Output the configuration key finalText.append(line).append("\n"); } } return finalText.toString().trim(); } /** * Fully reads a comment * * @param builder

The string builder to write to

* @param lines

The lines to read from

* @param startIndex

The index to start reading from

* @param commentIndentation

The indentation of the read comment

* @return

The index containing the next non-comment line

*/ private int readComment(@NotNull StringBuilder builder, @NotNull String[] lines, int startIndex, int commentIndentation) { for (int currentIndex = startIndex; currentIndex < lines.length; currentIndex++) { String line = lines[currentIndex]; String possibleComment = line.trim(); if (!line.contains(END_OF_COMMENT)) { possibleComment = possibleComment.replace(START_OF_COMMENT_LINE, ""); builder.append(addIndentation(commentIndentation)).append("# ").append(possibleComment).append("\n"); } else { return currentIndex; } } return startIndex; } /** * Gets a string containing the given indentation * * @param indentationSpaces

The number spaces to use for indentation

* @return

A string containing the number of spaces specified

*/ @NotNull private String addIndentation(int indentationSpaces) { return " ".repeat(Math.max(0, indentationSpaces)); } /** * Gets the indentation (number of spaces) of the given line * * @param line

The line to get indentation of

* @return

The number of spaces in the line's indentation

*/ private int getIndentation(@NotNull String line) { int spacesFound = 0; for (char aCharacter : line.toCharArray()) { if (aCharacter == ' ') { spacesFound++; } else { break; } } return spacesFound; } }