New Config code (Untested)

This commit is contained in:
Rsl1122 2018-12-16 18:09:21 +02:00
parent 557fa83177
commit 2fcb9590e5
5 changed files with 800 additions and 0 deletions

View File

@ -0,0 +1,86 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Risto Lahtela
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.djrapitops.plan.system.settings.config;
import com.djrapitops.plugin.utilities.Verify;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Configuration utility for storing settings in a .yml file.
* <p>
* Based on
* https://github.com/Rsl1122/Abstract-Plugin-Framework/blob/72e221d3571ef200727713d10d3684c51e9f469d/AbstractPluginFramework/api/src/main/java/com/djrapitops/plugin/config/Config.java
*
* @author Rsl1122
*/
public class Config extends ConfigNode {
private final Path configFilePath;
public Config(File configFile) {
super("", null, null);
File folder = configFile.getParentFile();
this.configFilePath = configFile.toPath();
try {
Verify.isTrue(folder.exists() || folder.mkdirs(), () ->
new FileNotFoundException("Folders could not be created for config file " + configFile.getAbsolutePath()));
Verify.isTrue(configFile.exists() || configFile.createNewFile(), () ->
new FileNotFoundException("Could not create file: " + configFile.getAbsolutePath()));
read();
save();
} catch (IOException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
public Config(File configFile, ConfigNode defaults) {
this(configFile);
copyMissing(defaults);
try {
save();
} catch (IOException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
Config() {
super("", null, null);
configFilePath = null;
}
public void read() throws IOException {
copyMissing(new ConfigReader(Files.newInputStream(configFilePath)).read());
}
@Override
public void save() throws IOException {
new ConfigWriter(configFilePath).write(this);
}
}

View File

@ -0,0 +1,266 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Risto Lahtela
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.djrapitops.plan.system.settings.config;
import java.io.IOException;
import java.util.*;
/**
* Represents a single node in a configuration file
* <p>
* Based on
* https://github.com/Rsl1122/Abstract-Plugin-Framework/blob/72e221d3571ef200727713d10d3684c51e9f469d/AbstractPluginFramework/api/src/main/java/com/djrapitops/plugin/config/ConfigNode.java
*
* @author Rsl1122
*/
public class ConfigNode {
protected final String key;
protected ConfigNode parent;
protected List<String> nodeOrder;
protected Map<String, ConfigNode> childNodes;
protected List<String> comment;
protected String value;
public ConfigNode(String key, ConfigNode parent, String value) {
this.key = key;
this.parent = parent;
this.value = value;
nodeOrder = new ArrayList<>();
childNodes = new HashMap<>();
comment = new ArrayList<>();
}
protected void updateParent(ConfigNode newParent) {
parent = newParent;
}
public Optional<ConfigNode> getNode(String path) {
String[] parts = path.split("\\.", 2);
String key = parts[0];
String leftover = parts[1];
if (leftover.isEmpty()) {
return Optional.ofNullable(childNodes.get(key));
} else {
return getNode(key).flatMap(child -> child.getNode(leftover));
}
}
protected void addNode(String path) {
ConfigNode newParent = this;
if (!path.isEmpty()) {
String[] parts = path.split("\\.", 2);
String key = parts[0];
String leftover = parts[1];
if (!childNodes.containsKey(key)) {
addChild(new ConfigNode(key, newParent, null));
}
ConfigNode child = childNodes.get(key);
child.addNode(leftover);
}
}
/**
* Remove a node at a certain path.
*
* @param path Path to the node that is up for removal.
* @return {@code true} if the node was present and is now removed. {@code false} if the path did not have a node.
*/
public boolean removeNode(String path) {
Optional<ConfigNode> node = getNode(path);
node.ifPresent(ConfigNode::remove);
return node.isPresent();
}
public void remove() {
parent.childNodes.remove(key);
parent.nodeOrder.remove(key);
updateParent(null);
}
protected void addChild(ConfigNode child) {
getNode(child.key).ifPresent(ConfigNode::remove);
childNodes.put(child.key, child);
nodeOrder.add(child.key);
child.updateParent(this);
}
protected void removeChild(ConfigNode child) {
removeNode(child.key);
}
/**
* Moves a node from old path to new path.
*
* @param oldPath Old path of the node.
* @param newPath New path of the node.
* @return {@code true} if the move was successful. {@code false} if the new node is not present
*/
public boolean moveChild(String oldPath, String newPath) {
Optional<ConfigNode> found = getNode(oldPath);
if (!found.isPresent()) {
return false;
}
addNode(newPath);
ConfigNode moveFrom = found.get();
ConfigNode moveTo = getNode(newPath).orElseThrow(() -> new IllegalStateException("Config node was not added properly: " + newPath));
ConfigNode oldParent = moveFrom.parent;
ConfigNode newParent = moveTo.parent;
oldParent.removeChild(moveFrom);
newParent.addChild(moveTo);
return getNode(newPath).isPresent();
}
public String getKey(boolean deep) {
if (deep && parent != null) {
String deepKey = parent.getKey(true) + "." + key;
if (deepKey.startsWith(".")) {
return deepKey.substring(1);
}
return deepKey;
}
return key;
}
public void sort() {
Collections.sort(nodeOrder);
}
public boolean reorder(List<String> newOrder) {
if (nodeOrder.containsAll(newOrder)) {
nodeOrder = newOrder;
return true;
}
return false;
}
/**
* Find the root node and save.
*
* @throws IOException If the save can not be performed.
*/
public void save() throws IOException {
ConfigNode root = this.parent;
while (root.parent != null) {
root = root.parent;
}
root.save();
}
public <T> void set(String path, T value) {
addNode(path);
ConfigNode node = getNode(path).orElseThrow(() -> new IllegalStateException("Config node was not added properly: " + path));
node.set(value);
}
public <T> void set(T value) {
if (value instanceof ConfigNode) {
addChild(((ConfigNode) value));
} else {
ConfigValueParser<T> parser = ConfigValueParser.getParserFor(value.getClass());
this.value = parser.decompose(value);
}
}
public List<String> getComment() {
return comment;
}
public void setComment(List<String> comment) {
this.comment = comment;
}
public List<String> getStringList() {
return new ConfigValueParser.StringListParser().compose(value);
}
public Integer getInteger() {
return new ConfigValueParser.IntegerParser().compose(value);
}
public Long getLong() {
return new ConfigValueParser.LongParser().compose(value);
}
public String getString() {
return new ConfigValueParser.StringParser().compose(value);
}
public boolean isTrue() {
return new ConfigValueParser.BooleanParser().compose(value);
}
public List<String> getStringList(String path) {
return getNode(path).map(ConfigNode::getStringList).orElse(new ArrayList<>());
}
public Integer getInteger(String path) {
return getNode(path).map(ConfigNode::getInteger).orElse(null);
}
public Long getLong(String path) {
return getNode(path).map(ConfigNode::getLong).orElse(null);
}
public String getString(String path) {
return getNode(path).map(ConfigNode::getString).orElse(null);
}
public boolean isTrue(String path) {
return getNode(path).map(ConfigNode::isTrue).orElse(false);
}
public void copyMissing(ConfigNode from) {
if (comment.size() < from.comment.size()) {
comment = from.comment;
}
if (value == null && from.value != null) {
value = from.value;
}
for (String key : from.nodeOrder) {
ConfigNode newChild = from.childNodes.get(key);
if (childNodes.containsKey(key)) {
ConfigNode oldChild = childNodes.get(key);
oldChild.copyMissing(newChild);
} else {
addChild(newChild);
}
}
}
protected int getNodeDepth() {
return parent != null ? parent.getNodeDepth() + 1 : 0;
}
}

View File

@ -0,0 +1,171 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.system.settings.config;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
/**
* Reader for parsing {@link Config} out of file-lines.
* <p>
* ConfigReader can read a single file at a time, so it is NOT thread safe.
*
* @author Rsl1122
*/
public class ConfigReader implements Closeable {
private static final ConfigValueParser.StringParser STRING_PARSER = new ConfigValueParser.StringParser();
private final InputStream in;
private final Scanner scanner;
private ConfigNode previousNode;
private ConfigNode parent;
public ConfigReader(InputStream in) {
this.in = in;
this.scanner = new Scanner(in);
}
public Config read() {
Config config = new Config();
previousNode = config;
parent = config;
while (scanner.hasNextLine()) {
String line = readNewLine();
// Determine where the node belongs
parent = findParent(previousNode.getNodeDepth(), findCurrentDepth(line));
previousNode = parseNode(line.trim());
}
return config;
}
private String readNewLine() {
String line = scanner.nextLine();
// Removing any dangling comments
int danglingComment = line.indexOf(" #");
if (danglingComment != -1) {
line = line.substring(0, danglingComment);
}
return line;
}
private ConfigNode parseNode(String line) {
if (line.startsWith("#")) {
return handleCommentLine(line);
}
String[] keyAndValue = line.split(":", 2);
if (keyAndValue.length <= 1) {
return handleMultiline(line);
}
String key = keyAndValue[0].trim();
String value = keyAndValue[1].trim();
return handleNewNode(key, value);
}
private ConfigNode handleMultiline(String line) {
if (line.startsWith("- ")) {
return handleListCase(line);
} else {
return handleMultilineString(line);
}
}
private ConfigNode handleCommentLine(String line) {
previousNode.comment.add(line.substring(1).trim());
return previousNode;
}
private ConfigNode handleMultilineString(String line) {
if (previousNode.value == null) {
previousNode.value = "";
}
// Append the new line to the end of the value.
previousNode.value += STRING_PARSER.compose(line.substring(2).trim());
return previousNode;
}
private ConfigNode handleNewNode(String key, String value) {
ConfigNode newNode = new ConfigNode(key, parent, STRING_PARSER.compose(value));
parent.addChild(newNode);
return newNode;
}
private ConfigNode handleListCase(String line) {
if (previousNode.value == null) {
previousNode.value = "";
}
// Append list item to the value.
previousNode.value += "\n- " + STRING_PARSER.compose(line.substring(2).trim());
return previousNode;
}
private ConfigNode findParent(int previousDepth, int currentDepth) {
if (previousDepth < currentDepth) {
return previousNode;
} else if (previousDepth > currentDepth) {
// Prevents incorrect indent in the case:
// 1:
// 2:
// 3:
// 1:
int helperDepth = previousDepth;
ConfigNode foundParent = parent;
while (helperDepth > currentDepth) {
helperDepth = parent.getNodeDepth();
foundParent = foundParent.parent; // Moves one level up the tree
}
return foundParent;
} else {
return parent;
}
}
private int findCurrentDepth(String line) {
int indent = readIndent(line);
int depth;
if (indent % 4 == 0) {
depth = indent / 4;
} else {
depth = (indent - (indent % 4)) / 4;
}
return depth;
}
private int readIndent(String line) {
int indentation = 0;
for (char c : line.toCharArray()) {
if (c == ' ') {
indentation++;
} else {
break;
}
}
return indentation;
}
@Override
public void close() throws IOException {
scanner.close();
in.close();
}
}

View File

@ -0,0 +1,168 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.system.settings.config;
import com.djrapitops.plugin.utilities.Verify;
import org.apache.commons.lang3.math.NumberUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Utilities for parsing config values.
*
* @param <T> Type of the object being parsed.
* @author Rsl1122
*/
public interface ConfigValueParser<T> {
static ConfigValueParser getParserFor(Class type) {
if (String.class.isAssignableFrom(type)) {
return new StringParser();
} else if (List.class.isAssignableFrom(type)) {
return new StringListParser();
} else if (Boolean.class.isAssignableFrom(type)) {
return new BooleanParser();
} else if (Long.class.isAssignableFrom(type)) {
return new LongParser();
} else if (Integer.class.isAssignableFrom(type)) {
return new IntegerParser();
}
return new StringParser();
}
/**
* Parse a String value in the config into the appropriate object.
*
* @param fromValue String value in the config.
* @return Config value or null if it could not be parsed.
*/
T compose(String fromValue);
/**
* Parse an object into a String value to save in the config.
*
* @param ofValue Value to save, not null.
* @return String format to save in the config.
* @throws IllegalArgumentException If null value is given.
*/
String decompose(T ofValue);
class StringParser implements ConfigValueParser<String> {
@Override
public String compose(String fromValue) {
String parsed = fromValue;
boolean surroundedWithSingleQuotes = parsed.startsWith("'") && parsed.endsWith("'");
boolean surroundedWithDoubleQuotes = parsed.startsWith("\"") && parsed.endsWith("\"");
if (surroundedWithSingleQuotes || surroundedWithDoubleQuotes) {
parsed = parsed.substring(1, parsed.length() - 1);
}
return parsed;
}
@Override
public String decompose(String ofValue) {
Verify.nullCheck(ofValue, () -> new IllegalArgumentException("Null value is not valid for saving"));
boolean surroundedWithSingleQuotes = ofValue.startsWith("'") && ofValue.endsWith("'");
if (surroundedWithSingleQuotes) {
return "\"" + ofValue + "\"";
}
boolean surroundedWithDoubleQuotes = ofValue.startsWith("\"") && ofValue.endsWith("\"");
if (surroundedWithDoubleQuotes) {
return "'" + ofValue + "'";
}
return ofValue;
}
}
class IntegerParser implements ConfigValueParser<Integer> {
@Override
public Integer compose(String fromValue) {
if (NumberUtils.isParsable(fromValue)) {
return NumberUtils.createInteger(fromValue);
}
return null;
}
@Override
public String decompose(Integer ofValue) {
Verify.nullCheck(ofValue, () -> new IllegalArgumentException("Null value is not valid for saving"));
return Integer.toString(ofValue);
}
}
class LongParser implements ConfigValueParser<Long> {
@Override
public Long compose(String fromValue) {
try {
return Long.parseLong(fromValue);
} catch (NumberFormatException e) {
return null;
}
}
@Override
public String decompose(Long ofValue) {
Verify.nullCheck(ofValue, () -> new IllegalArgumentException("Null value is not valid for saving"));
return Long.toString(ofValue);
}
}
class BooleanParser implements ConfigValueParser<Boolean> {
@Override
public Boolean compose(String fromValue) {
return Boolean.valueOf(fromValue);
}
@Override
public String decompose(Boolean ofValue) {
Verify.nullCheck(ofValue, () -> new IllegalArgumentException("Null value is not valid for saving"));
return Boolean.toString(ofValue);
}
}
class StringListParser implements ConfigValueParser<List<String>> {
private static final StringParser STRING_PARSER = new StringParser();
@Override
public List<String> compose(String fromValue) {
List<String> values = new ArrayList<>();
for (String line : fromValue.split("\\n")) {
// Removes '- ' in front of the value.
line = line.substring(2).trim();
// Handle quotes around the string
line = STRING_PARSER.compose(line);
if (!line.isEmpty()) {
values.add(line);
}
}
return values;
}
@Override
public String decompose(List<String> ofValue) {
Verify.nullCheck(ofValue, () -> new IllegalArgumentException("Null value is not valid for saving"));
StringBuilder decomposedString = new StringBuilder();
for (String value : ofValue) {
decomposedString.append("\n- ").append(STRING_PARSER.decompose(value));
}
return decomposedString.toString();
}
}
}

View File

@ -0,0 +1,109 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.system.settings.config;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Writer for parsing {@link Config} into file-lines.
* <p>
* ConfigReader can write a single file at a time, so it is NOT thread safe.
*
* @author Rsl1122
*/
public class ConfigWriter {
private final Path outputPath;
private int indent;
public ConfigWriter(Path outputPath) {
this.outputPath = outputPath;
}
public void write(ConfigNode writing) throws IOException {
ConfigNode storedParent = writing.parent;
writing.updateParent(null);
Files.write(outputPath, parseLines(writing), StandardCharsets.UTF_8);
writing.updateParent(storedParent);
}
private List<String> parseLines(ConfigNode writing) {
List<String> lines = new ArrayList<>();
indent = writing.getNodeDepth() * 4;
addComment(writing.comment, lines);
addValue(writing.key, writing.value, lines);
return lines;
}
private void addValue(String key, String value, Collection<String> lines) {
if (value == null) {
return;
}
if (value.contains("\n")) {
addListValue(key, value.split("\\n"), lines);
} else {
addNormalValue(key, value, lines);
}
}
private void addNormalValue(String key, String value, Collection<String> lines) {
StringBuilder lineBuilder = indentedBuilder().append(key).append(": ").append(value);
lines.add(lineBuilder.toString());
}
private void addListValue(String key, String[] listItems, Collection<String> lines) {
addNormalValue(key, "", lines);
for (String listItem : listItems) {
listItem = listItem.trim();
if (listItem.isEmpty()) {
continue;
}
StringBuilder lineBuilder = indentedBuilder().append(listItem);
lines.add(lineBuilder.toString());
}
}
private void addComment(Iterable<String> comments, Collection<String> lines) {
for (String comment : comments) {
StringBuilder lineBuilder = indentedBuilder().append("# ").append(comment);
lines.add(lineBuilder.toString());
}
}
private StringBuilder indentedBuilder() {
StringBuilder lineBuilder = new StringBuilder();
indent(indent, lineBuilder);
return lineBuilder;
}
private void indent(int indent, StringBuilder lineBuilder) {
for (int i = 0; i < indent; i++) {
lineBuilder.append(' ');
}
}
}