Plan/Plan/common/src/main/java/com/djrapitops/plan/settings/config/ConfigNode.java

466 lines
15 KiB
Java

/*
* The MIT License (MIT)
*
* Copyright (c) 2021 AuroraLS3
*
* 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.settings.config;
import com.djrapitops.plan.utilities.UnitSemaphoreAccessLock;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
/**
* Represents a single node in a configuration file
*
* @author AuroraLS3
*/
public class ConfigNode {
protected final UnitSemaphoreAccessLock nodeModificationLock = new UnitSemaphoreAccessLock();
protected final String key;
protected ConfigNode parent;
protected List<String> nodeOrder;
protected final 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 CopyOnWriteArrayList<>();
childNodes = new HashMap<>();
comment = new ArrayList<>();
}
protected void updateParent(ConfigNode newParent) {
parent = newParent;
}
public Optional<ConfigNode> getNode(String path) {
if (path == null) {
return Optional.empty();
}
String[] parts = splitPathInTwo(path);
String lookingFor = parts[0];
String leftover = parts[1];
if (leftover.isEmpty()) {
return Optional.ofNullable(childNodes.get(lookingFor));
} else {
return getNode(lookingFor).flatMap(child -> child.getNode(leftover));
}
}
private String[] splitPathInTwo(String path) {
String[] split = StringUtils.split(path, ".", 2);
if (split.length <= 1) {
return new String[]{split[0], ""};
}
return split;
}
public boolean contains(String path) {
return getNode(path).isPresent();
}
public ConfigNode addNode(String path) {
ConfigNode newParent = this;
if (path != null && !path.isEmpty()) {
String[] parts = splitPathInTwo(path);
String lookingFor = parts[0];
String leftover = parts[1];
// Add a new child
ConfigNode child;
if (!childNodes.containsKey(lookingFor)) {
child = addChild(new ConfigNode(lookingFor, newParent, null));
} else {
child = childNodes.get(lookingFor);
}
// If the path ends return the leaf node
// Otherwise continue recursively.
return leftover.isEmpty() ? child : child.addNode(leftover);
}
throw new IllegalArgumentException("Can not add a node with path '" + path + "'");
}
/**
* 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() {
if (parent == null) {
throw new IllegalStateException("Can not remove root node from a tree.");
}
nodeModificationLock.enter();
parent.nodeOrder.remove(key);
parent.childNodes.remove(key);
nodeModificationLock.exit();
updateParent(null);
// Remove children recursively to avoid memory leaks
nodeOrder.stream()
.sorted() // will use internal state and prevent Concurrent modification of underlying list
.map(childNodes::get)
.filter(Objects::nonNull)
.forEach(ConfigNode::remove);
}
/**
* Add a new child ConfigNode.
*
* @param child ConfigNode to add.
* If from another config tree, the parent is 'cut', which breaks the old tree traversal.
* @return Return the node given, now part of this tree.
*/
protected ConfigNode addChild(ConfigNode child) {
getNode(child.key).ifPresent(ConfigNode::remove);
nodeModificationLock.enter();
childNodes.put(child.key, child);
nodeOrder.add(child.key);
nodeModificationLock.exit();
child.updateParent(this);
return child;
}
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.isEmpty()) {
return false;
}
ConfigNode moveFrom = found.get();
ConfigNode moveTo = addNode(newPath);
moveTo.copyAll(moveFrom);
removeNode(oldPath);
return getNode(newPath).isPresent();
}
public String getKey(boolean deep) {
if (deep) {
String deepKey = parent != null ? parent.getKey(true) + "." + key : "";
if (deepKey.startsWith(".")) {
return deepKey.substring(1);
}
return deepKey;
}
return key;
}
private String getEnvironmentVariableKey() {
String deepKey = parent != null ? parent.getKey(true) + "." + key : "";
if (deepKey.startsWith(".")) {
deepKey = deepKey.substring(1);
}
return "PLAN_" + StringUtils.replaceChars(StringUtils.upperCase(deepKey), '.', '_');
}
public void sort() {
Collections.sort(nodeOrder);
}
public void reorder(List<String> newOrder) {
nodeModificationLock.enter();
List<String> oldOrder = nodeOrder;
nodeOrder = new ArrayList<>();
for (String childKey : newOrder) {
if (childNodes.containsKey(childKey)) {
nodeOrder.add(childKey);
}
}
// Add those that were not in the new order, but are in the old order.
oldOrder.removeAll(nodeOrder);
nodeOrder.addAll(oldOrder);
nodeModificationLock.exit();
}
/**
* 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).set(value);
}
public <T> void set(T value) {
if (value == null) {
this.value = null;
} else if (value instanceof ConfigNode) {
copyAll((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;
}
private String getEnvironmentVariable() {
String key = getEnvironmentVariableKey();
String variable = System.getenv(key);
Map<String, String> env = System.getenv();
return variable;
}
public List<String> getStringList() {
String environmentVariable = getEnvironmentVariable();
if (environmentVariable != null) return new ConfigValueParser.StringListParser().compose(environmentVariable);
return value == null ? Collections.emptyList()
: new ConfigValueParser.StringListParser().compose(value);
}
public Integer getInteger() {
String environmentVariable = getEnvironmentVariable();
if (environmentVariable != null) return new ConfigValueParser.IntegerParser().compose(environmentVariable);
return value == null ? null
: new ConfigValueParser.IntegerParser().compose(value);
}
public Long getLong() {
String environmentVariable = getEnvironmentVariable();
if (environmentVariable != null) return new ConfigValueParser.LongParser().compose(environmentVariable);
return value == null ? null
: new ConfigValueParser.LongParser().compose(value);
}
public String getString() {
String environmentVariable = getEnvironmentVariable();
if (environmentVariable != null) return new ConfigValueParser.StringParser().compose(environmentVariable);
return value == null ? null
: new ConfigValueParser.StringParser().compose(value);
}
public Double getDouble() {
String environmentVariable = getEnvironmentVariable();
if (environmentVariable != null) return new ConfigValueParser.DoubleParser().compose(environmentVariable);
return value == null ? null
: new ConfigValueParser.DoubleParser().compose(value);
}
public boolean getBoolean() {
String environmentVariable = getEnvironmentVariable();
if (environmentVariable != null) return new ConfigValueParser.BooleanParser().compose(environmentVariable);
return new ConfigValueParser.BooleanParser().compose(value);
}
public List<String> getStringList(String path) {
return getNode(path).map(ConfigNode::getStringList).orElse(Collections.emptyList());
}
/**
* Return values in a Map.
*
* @param fullKeys Should the key be full keys of the Config node.
* @return Map with Config key - ConfigNode#getString.
*/
public Map<String, String> getStringMap(boolean fullKeys) {
return childNodes.values().stream()
.collect(Collectors.toMap(node -> node.getKey(fullKeys), ConfigNode::getString));
}
/**
* @return List of config keys
*/
public List<String> getConfigPaths() {
ArrayDeque<ConfigNode> dfs = new ArrayDeque<>();
dfs.push(this);
List<String> configPaths = new ArrayList<>();
while (!dfs.isEmpty()) {
ConfigNode next = dfs.pop();
if (next.isLeafNode()) {
configPaths.add(next.getKey(true));
} else {
dfs.addAll(next.getChildren());
}
}
return configPaths;
}
public <T> List<T> dfs(BiConsumer<ConfigNode, List<T>> accessVisitor) {
ArrayDeque<ConfigNode> dfs = new ArrayDeque<>();
dfs.push(this);
List<T> result = new ArrayList<>();
while (!dfs.isEmpty()) {
ConfigNode next = dfs.pop();
accessVisitor.accept(next, result);
dfs.addAll(next.getChildren());
}
return result;
}
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 getBoolean(String path) {
return getNode(path).map(ConfigNode::getBoolean).orElse(false);
}
public Double getDouble(String path) {
return getNode(path).map(ConfigNode::getDouble).orElse(null);
}
public void copyMissing(ConfigNode from) {
// Override comment conditionally
if (comment.size() < from.comment.size()) {
comment = from.comment;
}
// Override value conditionally
boolean currentValueIsMissing = value == null || value.isEmpty();
boolean otherNodeHasValue = from.value != null && !from.value.isEmpty();
if (currentValueIsMissing && otherNodeHasValue) {
value = from.value;
}
// Copy all nodes from 'from'
for (String childKey : from.nodeOrder) {
ConfigNode newChild = from.childNodes.get(childKey);
// Copy values recursively to children
ConfigNode created = addNode(childKey);
created.copyMissing(newChild);
}
}
public void copyAll(ConfigNode from) {
// Override comment and value unconditionally.
comment = from.comment;
value = from.value;
// Copy all nodes from 'from'
for (String childKey : from.nodeOrder) {
ConfigNode newChild = from.childNodes.get(childKey);
// Copy values recursively to children
ConfigNode created = addNode(childKey);
created.copyAll(newChild);
}
}
public void copyValue(ConfigNode from) {
comment = from.comment;
value = from.value;
}
protected int getNodeDepth() {
return parent != null ? parent.getNodeDepth() + 1 : -1; // Root node is -1
}
public ConfigNode getParent() {
return parent;
}
public boolean isLeafNode() {
return nodeOrder.isEmpty();
}
protected List<String> getNodeOrder() {
return nodeOrder;
}
public Collection<ConfigNode> getChildren() {
return childNodes.values();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ConfigNode)) return false;
ConfigNode that = (ConfigNode) o;
return Objects.equals(key, that.key) &&
nodeOrder.equals(that.nodeOrder) &&
childNodes.equals(that.childNodes) &&
comment.equals(that.comment) &&
Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(key, childNodes, comment, value);
}
@Override
public String toString() {
return "{'" + value + "' " + (!childNodes.isEmpty() ? childNodes : "") + '}';
}
}