794 lines
24 KiB
Java
794 lines
24 KiB
Java
package com.songoda.core.configuration;
|
|
|
|
import com.songoda.core.utils.TextUtils;
|
|
import org.apache.commons.lang.Validate;
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.configuration.InvalidConfigurationException;
|
|
import org.bukkit.configuration.file.YamlConstructor;
|
|
import org.bukkit.configuration.file.YamlRepresenter;
|
|
import org.bukkit.plugin.Plugin;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.yaml.snakeyaml.DumperOptions;
|
|
import org.yaml.snakeyaml.Yaml;
|
|
import org.yaml.snakeyaml.error.YAMLException;
|
|
import org.yaml.snakeyaml.representer.Representer;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.Reader;
|
|
import java.io.StringReader;
|
|
import java.io.StringWriter;
|
|
import java.io.Writer;
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Timer;
|
|
import java.util.TimerTask;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* Configuration settings for a plugin
|
|
*/
|
|
public class Config extends ConfigSection {
|
|
/*
|
|
Serialization notes:
|
|
// implements ConfigurationSerializable:
|
|
//public Map<String, Object> serialize();
|
|
|
|
// Class must contain one of:
|
|
// public static Object deserialize(@NotNull Map<String, ?> args);
|
|
// public static valueOf(Map<String, ?> args);
|
|
// public new (Map<String, ?> args)
|
|
*/
|
|
protected static final String BLANK_CONFIG = "{}\n";
|
|
|
|
protected File file;
|
|
protected final ConfigFileConfigurationAdapter config = new ConfigFileConfigurationAdapter(this);
|
|
protected Comment headerComment = null;
|
|
protected Comment footerComment = null;
|
|
final String dirName, fileName;
|
|
final Plugin plugin;
|
|
final DumperOptions yamlOptions = new DumperOptions();
|
|
final Representer yamlRepresenter = new YamlRepresenter();
|
|
final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions);
|
|
Charset defaultCharset = StandardCharsets.UTF_8;
|
|
SaveTask saveTask;
|
|
Timer autosaveTimer;
|
|
|
|
////////////// Config settings ////////////////
|
|
/**
|
|
* save file whenever a change is made
|
|
*/
|
|
boolean autosave = false;
|
|
|
|
/**
|
|
* time in seconds to start a save after a change is made
|
|
*/
|
|
int autosaveInterval = 60;
|
|
|
|
/**
|
|
* remove nodes not defined in defaults
|
|
*/
|
|
boolean autoremove = false;
|
|
|
|
/**
|
|
* load comments when loading the file
|
|
*/
|
|
boolean loadComments = true;
|
|
|
|
/**
|
|
* Default comment applied to config nodes
|
|
*/
|
|
ConfigFormattingRules.CommentStyle defaultNodeCommentFormat = ConfigFormattingRules.CommentStyle.SIMPLE;
|
|
|
|
/**
|
|
* Default comment applied to section nodes
|
|
*/
|
|
ConfigFormattingRules.CommentStyle defaultSectionCommentFormat = ConfigFormattingRules.CommentStyle.SPACED;
|
|
|
|
/**
|
|
* Extra lines to put between root nodes
|
|
*/
|
|
int rootNodeSpacing = 1;
|
|
|
|
/**
|
|
* Extra lines to put in front of comments. <br>
|
|
* This is separate from rootNodeSpacing, if applicable.
|
|
*/
|
|
int commentSpacing = 1;
|
|
|
|
public Config() {
|
|
this.plugin = null;
|
|
this.file = null;
|
|
|
|
dirName = null;
|
|
fileName = null;
|
|
}
|
|
|
|
public Config(@NotNull File file) {
|
|
this.plugin = null;
|
|
this.file = file.getAbsoluteFile();
|
|
|
|
dirName = null;
|
|
fileName = null;
|
|
}
|
|
|
|
public Config(@NotNull Plugin plugin) {
|
|
this.plugin = plugin;
|
|
|
|
dirName = null;
|
|
fileName = null;
|
|
}
|
|
|
|
public Config(@NotNull Plugin plugin, @NotNull String file) {
|
|
this.plugin = plugin;
|
|
|
|
dirName = null;
|
|
fileName = file;
|
|
}
|
|
|
|
public Config(@NotNull Plugin plugin, @Nullable String directory, @NotNull String file) {
|
|
this.plugin = plugin;
|
|
|
|
dirName = directory;
|
|
fileName = file;
|
|
}
|
|
|
|
@NotNull
|
|
public ConfigFileConfigurationAdapter getFileConfig() {
|
|
return config;
|
|
}
|
|
|
|
@NotNull
|
|
public File getFile() {
|
|
if (file == null) {
|
|
if (dirName != null) {
|
|
this.file = new File(plugin.getDataFolder() + dirName, fileName != null ? fileName : "config.yml");
|
|
} else {
|
|
this.file = new File(plugin.getDataFolder(), fileName != null ? fileName : "config.yml");
|
|
}
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
public Charset getDefaultCharset() {
|
|
return defaultCharset;
|
|
}
|
|
|
|
/**
|
|
* Set the Charset that will be used to save this config
|
|
*
|
|
* @param defaultCharset Charset to use
|
|
*
|
|
* @return this class
|
|
*/
|
|
public Config setDefaultCharset(Charset defaultCharset) {
|
|
this.defaultCharset = defaultCharset;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the default charset to use UTF-16
|
|
*
|
|
* @return this class
|
|
*/
|
|
public Config setUseUTF16() {
|
|
this.defaultCharset = StandardCharsets.UTF_16;
|
|
return this;
|
|
}
|
|
|
|
public boolean getLoadComments() {
|
|
return loadComments;
|
|
}
|
|
|
|
/**
|
|
* Should comments from the config file be loaded when loading?
|
|
*
|
|
* @param loadComments set to false if you do not want to preserve node comments
|
|
*/
|
|
public void setLoadComments(boolean loadComments) {
|
|
this.loadComments = loadComments;
|
|
}
|
|
|
|
public boolean getAutosave() {
|
|
return autosave;
|
|
}
|
|
|
|
/**
|
|
* Should the configuration automatically save whenever it's been changed? <br>
|
|
* All saves are done asynchronously, so this should not impact server performance.
|
|
*
|
|
* @param autosave set to true if autosaving is enabled.
|
|
*
|
|
* @return this class
|
|
*/
|
|
@NotNull
|
|
public Config setAutosave(boolean autosave) {
|
|
this.autosave = autosave;
|
|
return this;
|
|
}
|
|
|
|
public int getAutosaveInterval() {
|
|
return autosaveInterval;
|
|
}
|
|
|
|
/**
|
|
* If autosave is enabled, this is the delay between a change and when the save is started. <br>
|
|
* If the configuration is changed within this period, the timer is not reset.
|
|
*
|
|
* @param autosaveInterval time in seconds
|
|
*
|
|
* @return this class
|
|
*/
|
|
@NotNull
|
|
public Config setAutosaveInterval(int autosaveInterval) {
|
|
this.autosaveInterval = autosaveInterval;
|
|
return this;
|
|
}
|
|
|
|
public boolean getAutoremove() {
|
|
return autoremove;
|
|
}
|
|
|
|
/**
|
|
* This setting is used to prevent users to from adding extraneous settings
|
|
* to the config and to remove deprecated settings. <br>
|
|
* If this is enabled, the config will delete any nodes that are not defined
|
|
* as a default setting.
|
|
*
|
|
* @param autoremove Remove settings that don't exist as defaults
|
|
*
|
|
* @return this class
|
|
*/
|
|
@NotNull
|
|
public Config setAutoremove(boolean autoremove) {
|
|
this.autoremove = autoremove;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Default comment applied to config nodes
|
|
*/
|
|
@Nullable
|
|
public ConfigFormattingRules.CommentStyle getDefaultNodeCommentFormat() {
|
|
return defaultNodeCommentFormat;
|
|
}
|
|
|
|
/**
|
|
* Default comment applied to config nodes
|
|
*
|
|
* @return this config
|
|
*/
|
|
@NotNull
|
|
public Config setDefaultNodeCommentFormat(@Nullable ConfigFormattingRules.CommentStyle defaultNodeCommentFormat) {
|
|
this.defaultNodeCommentFormat = defaultNodeCommentFormat;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Default comment applied to section nodes
|
|
*/
|
|
@Nullable
|
|
public ConfigFormattingRules.CommentStyle getDefaultSectionCommentFormat() {
|
|
return defaultSectionCommentFormat;
|
|
}
|
|
|
|
/**
|
|
* Default comment applied to section nodes
|
|
*
|
|
* @return this config
|
|
*/
|
|
@NotNull
|
|
public Config setDefaultSectionCommentFormat(@Nullable ConfigFormattingRules.CommentStyle defaultSectionCommentFormat) {
|
|
this.defaultSectionCommentFormat = defaultSectionCommentFormat;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Extra lines to put between root nodes
|
|
*/
|
|
public int getRootNodeSpacing() {
|
|
return rootNodeSpacing;
|
|
}
|
|
|
|
/**
|
|
* Extra lines to put between root nodes
|
|
*
|
|
* @return this config
|
|
*/
|
|
@NotNull
|
|
public Config setRootNodeSpacing(int rootNodeSpacing) {
|
|
this.rootNodeSpacing = rootNodeSpacing;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Extra lines to put in front of comments. <br>
|
|
* This is separate from rootNodeSpacing, if applicable.
|
|
*/
|
|
public int getCommentSpacing() {
|
|
return commentSpacing;
|
|
}
|
|
|
|
/**
|
|
* Extra lines to put in front of comments. <br>
|
|
* This is separate from rootNodeSpacing, if applicable.
|
|
*
|
|
* @return this config
|
|
*/
|
|
@NotNull
|
|
public Config setCommentSpacing(int commentSpacing) {
|
|
this.commentSpacing = commentSpacing;
|
|
return this;
|
|
}
|
|
|
|
@NotNull
|
|
public Config setHeader(@NotNull String... description) {
|
|
if (description.length == 0) {
|
|
headerComment = null;
|
|
} else {
|
|
headerComment = new Comment(description);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
@NotNull
|
|
public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @NotNull String... description) {
|
|
if (description.length == 0) {
|
|
headerComment = null;
|
|
} else {
|
|
headerComment = new Comment(commentStyle, description);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
@NotNull
|
|
public Config setHeader(@Nullable List<String> description) {
|
|
if (description == null || description.isEmpty()) {
|
|
headerComment = null;
|
|
} else {
|
|
headerComment = new Comment(description);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
@NotNull
|
|
public Config setHeader(@Nullable ConfigFormattingRules.CommentStyle commentStyle, @Nullable List<String> description) {
|
|
if (description == null || description.isEmpty()) {
|
|
headerComment = null;
|
|
} else {
|
|
headerComment = new Comment(commentStyle, description);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
@NotNull
|
|
public List<String> getHeader() {
|
|
if (headerComment != null) {
|
|
return headerComment.getLines();
|
|
}
|
|
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
public Config clearConfig(boolean clearDefaults) {
|
|
root.values.clear();
|
|
root.configComments.clear();
|
|
|
|
if (clearDefaults) {
|
|
root.defaultComments.clear();
|
|
root.defaults.clear();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public Config clearDefaults() {
|
|
root.defaultComments.clear();
|
|
root.defaults.clear();
|
|
|
|
return this;
|
|
}
|
|
|
|
public boolean load() {
|
|
return load(getFile());
|
|
}
|
|
|
|
public boolean load(@NotNull File file) {
|
|
Validate.notNull(file, "File cannot be null");
|
|
if (file.exists()) {
|
|
try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file))) {
|
|
Charset charset = TextUtils.detectCharset(stream, StandardCharsets.UTF_8);
|
|
|
|
// upgrade charset if file was saved in a more complex format
|
|
if (charset == StandardCharsets.UTF_16BE || charset == StandardCharsets.UTF_16LE) {
|
|
defaultCharset = StandardCharsets.UTF_16;
|
|
}
|
|
|
|
this.load(new InputStreamReader(stream, charset));
|
|
|
|
return true;
|
|
} catch (IOException | InvalidConfigurationException ex) {
|
|
(plugin != null ? plugin.getLogger() : Bukkit.getLogger()).log(Level.SEVERE, "Failed to load config file: " + file.getName(), ex);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void load(@NotNull Reader reader) throws IOException, InvalidConfigurationException {
|
|
StringBuilder builder = new StringBuilder();
|
|
|
|
try (BufferedReader input = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader)) {
|
|
String line;
|
|
boolean firstLine = true;
|
|
while ((line = input.readLine()) != null) {
|
|
if (firstLine) {
|
|
line = line.replaceAll("[\uFEFF\uFFFE\u200B]", ""); // clear BOM markers
|
|
firstLine = false;
|
|
}
|
|
builder.append(line).append('\n');
|
|
}
|
|
}
|
|
|
|
this.loadFromString(builder.toString());
|
|
}
|
|
|
|
public void loadFromString(@NotNull String contents) throws InvalidConfigurationException {
|
|
Map<?, ?> input;
|
|
|
|
try {
|
|
input = this.yaml.load(contents);
|
|
} catch (YAMLException e2) {
|
|
throw new InvalidConfigurationException(e2);
|
|
} catch (ClassCastException e3) {
|
|
throw new InvalidConfigurationException("Top level is not a Map.");
|
|
}
|
|
|
|
if (input != null) {
|
|
if (loadComments) {
|
|
this.parseComments(contents, input);
|
|
}
|
|
|
|
this.convertMapsToSections(input, this);
|
|
}
|
|
}
|
|
|
|
protected void convertMapsToSections(@NotNull Map<?, ?> input, @NotNull ConfigSection section) {
|
|
// TODO: make this non-recursive
|
|
for (Map.Entry<?, ?> entry : input.entrySet()) {
|
|
String key = entry.getKey().toString();
|
|
Object value = entry.getValue();
|
|
|
|
if (value instanceof Map) {
|
|
this.convertMapsToSections((Map<?, ?>) value, section.createSection(key));
|
|
continue;
|
|
}
|
|
|
|
section.set(key, value);
|
|
}
|
|
}
|
|
|
|
protected void parseComments(@NotNull String contents, @NotNull Map<?, ?> input) {
|
|
// if starts with a comment, load all nonbreaking comments as a header
|
|
// then load all comments and assign to the next valid node loaded
|
|
// (Only load comments that are on their own line)
|
|
|
|
BufferedReader in = new BufferedReader(new StringReader(contents));
|
|
String line;
|
|
boolean insideScalar = false;
|
|
boolean firstNode = true;
|
|
int index = 0;
|
|
LinkedList<String> currentPath = new LinkedList<>();
|
|
ArrayList<String> commentBlock = new ArrayList<>();
|
|
|
|
try {
|
|
while ((line = in.readLine()) != null) {
|
|
if (line.isEmpty()) {
|
|
if (firstNode && !commentBlock.isEmpty()) {
|
|
// header comment
|
|
firstNode = false;
|
|
headerComment = Comment.loadComment(commentBlock);
|
|
commentBlock.clear();
|
|
}
|
|
continue;
|
|
} else if (line.trim().startsWith("#")) {
|
|
// only load full-line comments
|
|
commentBlock.add(line.trim());
|
|
continue;
|
|
}
|
|
|
|
// check to see if this is a line that we can process
|
|
int lineOffset = getOffset(line);
|
|
insideScalar &= lineOffset <= index;
|
|
Matcher m;
|
|
if (!insideScalar && (m = yamlNode.matcher(line)).find()) {
|
|
// we found a config node! ^.^
|
|
// check to see what the full path is
|
|
int depth = (m.group(1).length() / indentation);
|
|
while (depth < currentPath.size()) {
|
|
currentPath.removeLast();
|
|
}
|
|
currentPath.add(m.group(2));
|
|
|
|
// do we have a comment for this node?
|
|
if (!commentBlock.isEmpty()) {
|
|
String path = currentPath.stream().collect(Collectors.joining(String.valueOf(pathChar)));
|
|
Comment comment = Comment.loadComment(commentBlock);
|
|
commentBlock.clear();
|
|
setComment(path, comment);
|
|
}
|
|
|
|
firstNode = false; // we're no longer on the first node
|
|
|
|
// ignore scalars
|
|
index = lineOffset;
|
|
if (m.group(3).trim().equals("|") || m.group(3).trim().equals(">")) {
|
|
insideScalar = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!commentBlock.isEmpty()) {
|
|
footerComment = Comment.loadComment(commentBlock);
|
|
commentBlock.clear();
|
|
}
|
|
} catch (IOException ex) {
|
|
Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error parsing config comment", ex);
|
|
}
|
|
}
|
|
|
|
public void deleteNonDefaultSettings() {
|
|
// Delete old config values (thread-safe)
|
|
List<String> defaultKeys = Arrays.asList(defaults.keySet().toArray(new String[0]));
|
|
|
|
for (String key : values.keySet().toArray(new String[0])) {
|
|
if (!defaultKeys.contains(key)) {
|
|
values.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onChange() {
|
|
if (autosave) {
|
|
delaySave();
|
|
}
|
|
}
|
|
|
|
public void delaySave() {
|
|
// save async even if no plugin or if plugin disabled
|
|
if (saveTask == null && (changed || hasNewDefaults())) {
|
|
autosaveTimer = new Timer((plugin != null ? plugin.getName() + "-ConfigSave-" : "ConfigSave-") + getFile().getName());
|
|
autosaveTimer.schedule(saveTask = new SaveTask(), autosaveInterval * 1000L);
|
|
}
|
|
}
|
|
|
|
public boolean saveChanges() {
|
|
boolean saved = true;
|
|
|
|
if (changed || hasNewDefaults()) {
|
|
saved = save();
|
|
}
|
|
|
|
if (saveTask != null) {
|
|
//Close Threads
|
|
saveTask.cancel();
|
|
autosaveTimer.cancel();
|
|
saveTask = null;
|
|
autosaveTimer = null;
|
|
}
|
|
|
|
return saved;
|
|
}
|
|
|
|
boolean hasNewDefaults() {
|
|
if (file != null && !file.exists()) return true;
|
|
|
|
for (String def : defaults.keySet()) {
|
|
if (!values.containsKey(def)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public boolean save() {
|
|
if (saveTask != null) {
|
|
//Close Threads
|
|
saveTask.cancel();
|
|
autosaveTimer.cancel();
|
|
saveTask = null;
|
|
autosaveTimer = null;
|
|
}
|
|
|
|
return save(getFile());
|
|
}
|
|
|
|
public boolean save(@NotNull String file) {
|
|
Validate.notNull(file, "File cannot be null");
|
|
return this.save(new File(file));
|
|
}
|
|
|
|
public boolean save(@NotNull File file) {
|
|
Validate.notNull(file, "File cannot be null");
|
|
|
|
if (file.getParentFile() != null && !file.getParentFile().exists()) {
|
|
file.getParentFile().mkdirs();
|
|
}
|
|
|
|
String data = this.saveToString();
|
|
try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), defaultCharset)) {
|
|
writer.write(data);
|
|
} catch (IOException ex) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@NotNull
|
|
public String saveToString() {
|
|
try {
|
|
if (autoremove) {
|
|
deleteNonDefaultSettings();
|
|
}
|
|
|
|
yamlOptions.setIndent(indentation);
|
|
yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
|
yamlOptions.setSplitLines(false);
|
|
yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
|
|
|
StringWriter str = new StringWriter();
|
|
|
|
if (headerComment != null) {
|
|
headerComment.writeComment(str, 0, ConfigFormattingRules.CommentStyle.BLOCKED);
|
|
str.write("\n"); // add one space after the header
|
|
}
|
|
|
|
String dump = yaml.dump(this.getValues(false));
|
|
if (!dump.equals(BLANK_CONFIG)) {
|
|
writeComments(dump, str);
|
|
}
|
|
|
|
if (footerComment != null) {
|
|
str.write("\n");
|
|
footerComment.writeComment(str, 0, ConfigFormattingRules.CommentStyle.BLOCKED);
|
|
}
|
|
|
|
return str.toString();
|
|
} catch (Throwable ex) {
|
|
Logger.getLogger(Config.class.getName()).log(Level.SEVERE, "Error saving config", ex);
|
|
delaySave();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
protected final Pattern yamlNode = Pattern.compile("^( *)([^:{}\\[\\],&*#?|\\-<>=!%@`]+):(.*)$");
|
|
|
|
protected void writeComments(String data, Writer out) throws IOException {
|
|
// line-by-line apply line spacing formatting and comments per-node
|
|
BufferedReader in = new BufferedReader(new StringReader(data));
|
|
|
|
String line;
|
|
boolean insideScalar = false;
|
|
boolean firstNode = true;
|
|
int index = 0;
|
|
|
|
LinkedList<String> currentPath = new LinkedList<>();
|
|
while ((line = in.readLine()) != null) {
|
|
// ignore comments and empty lines (there shouldn't be any, but just in case)
|
|
if (line.trim().startsWith("#") || line.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
// check to see if this is a line that we can process
|
|
int lineOffset = getOffset(line);
|
|
insideScalar &= lineOffset <= index;
|
|
Matcher m;
|
|
if (!insideScalar && (m = yamlNode.matcher(line)).find()) {
|
|
// we found a config node! ^.^
|
|
// check to see what the full path is
|
|
int depth = (m.group(1).length() / indentation);
|
|
while (depth < currentPath.size()) {
|
|
currentPath.removeLast();
|
|
}
|
|
|
|
currentPath.add(m.group(2));
|
|
String path = currentPath.stream().collect(Collectors.joining(String.valueOf(pathChar)));
|
|
|
|
// if this is a root-level node, apply extra spacing if we aren't the first node
|
|
if (!firstNode && depth == 0 && rootNodeSpacing > 0) {
|
|
out.write((new String(new char[rootNodeSpacing])).replace("\0", "\n")); // yes it's silly, but it works :>
|
|
}
|
|
firstNode = false; // we're no longer on the first node
|
|
|
|
// insert the relavant comment
|
|
Comment comment = getComment(path);
|
|
if (comment != null) {
|
|
// add spacing between previous nodes and comments
|
|
if (depth != 0) {
|
|
out.write((new String(new char[commentSpacing])).replace("\0", "\n"));
|
|
}
|
|
|
|
// formatting style for this node
|
|
ConfigFormattingRules.CommentStyle style = comment.getCommentStyle();
|
|
if (style == null) {
|
|
// check to see what type of node this is
|
|
if (!m.group(3).trim().isEmpty()) {
|
|
// setting node
|
|
style = defaultNodeCommentFormat;
|
|
} else {
|
|
// probably a section? (need to peek ahead to check if this is a list)
|
|
in.mark(1000);
|
|
String nextLine = in.readLine().trim();
|
|
in.reset();
|
|
if (nextLine.startsWith("-")) {
|
|
// not a section :P
|
|
style = defaultNodeCommentFormat;
|
|
} else {
|
|
style = defaultSectionCommentFormat;
|
|
}
|
|
}
|
|
}
|
|
|
|
// write it down!
|
|
comment.writeComment(out, lineOffset, style);
|
|
}
|
|
|
|
// ignore scalars
|
|
index = lineOffset;
|
|
if (m.group(3).trim().equals("|") || m.group(3).trim().equals(">")) {
|
|
insideScalar = true;
|
|
}
|
|
}
|
|
|
|
out.write(line);
|
|
out.write("\n");
|
|
}
|
|
}
|
|
|
|
protected static int getOffset(String s) {
|
|
char[] chars = s.toCharArray();
|
|
for (int i = 0; i < chars.length; ++i) {
|
|
if (chars[i] != ' ') {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
class SaveTask extends TimerTask {
|
|
@Override
|
|
public void run() {
|
|
saveChanges();
|
|
}
|
|
}
|
|
}
|