package com.bergerkiller.bukkit.common.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.bukkit.configuration.MemorySection;
import org.bukkit.configuration.file.YamlConfiguration;
import com.bergerkiller.bukkit.common.utils.LogicUtil;
import com.bergerkiller.bukkit.common.utils.ParseUtil;
import com.bergerkiller.bukkit.common.utils.StringUtil;
@SuppressWarnings({ "unchecked", "rawtypes" })
public class ConfigurationNode implements Cloneable {
private final MemorySection source;
private final Map<String, String> headers;
private final Set<String> readkeys;
public ConfigurationNode() {
this(new HashSet<String>(), new HashMap<String, String>(), new YamlConfiguration());
}
private ConfigurationNode(ConfigurationNode source, String root) {
this.readkeys = source.readkeys;
this.headers = source.headers;
MemorySection sect = (MemorySection) source.source.getConfigurationSection(root);
if (sect == null) {
this.source = (MemorySection) source.source.createSection(root);
} else {
this.source = sect;
}
this.setRead();
}
private ConfigurationNode(final Set<String> readkeys, final Map<String, String> headers, final MemorySection source) {
this.readkeys = readkeys;
this.source = source;
this.headers = headers;
}
/**
* Checks if this node has a parent, or if it a root node
*
* @return True if it has a parent, False if not
*/
public boolean hasParent() {
return this.source.getParent() != null;
}
/**
* Checks if this node has sub-nodes or values
*
* @return True if this node is empty, False if not
*/
public boolean isEmpty() {
return this.getKeys().isEmpty();
}
/**
* Gets the parent configuration node of this Node
*
* @return parent node
*/
public ConfigurationNode getParent() {
MemorySection sec = (MemorySection) this.source.getParent();
if (sec == null) {
return null;
}
return new ConfigurationNode(this.readkeys, this.headers, sec);
}
/**
* Gets the full path of a value or node relative to this Configuration Node
*
* @param append path, the path to the value or node relative to this Node
* @return The full path
*/
public String getPath(String append) {
String p = this.getPath();
if (LogicUtil.nullOrEmpty(append)) {
return p;
}
if (LogicUtil.nullOrEmpty(p)) {
return append;
}
return p + "." + append;
}
/**
* Gets the full path of this Configuration Node
*
* @return node path
*/
public String getPath() {
return this.source.getCurrentPath();
}
/**
* Gets the name of this Configuration Node
*
* @return node name
*/
public String getName() {
return this.source.getName();
}
/**
* Gets the header of this Node
*
* @return The header
*/
public String getHeader() {
return this.headers.get(this.getPath());
}
/**
* Gets the header of a value or node
*
* @param path to the value or node
* @return Header at the path specified
*/
public String getHeader(String path) {
return this.headers.get(this.getPath(path));
}
/**
* Removes the header for this Node
*/
public void removeHeader() {
this.setHeader(null);
}
/**
* Removes the header for a certain value or node
*
* @param path to the value or node
*/
public void removeHeader(String path) {
this.setHeader(path, null);
}
/**
* Sets the header displayed above this configuration node
*
* @param header to set to
*/
public void setHeader(String header) {
this.setHeader("", header);
}
/**
* Sets the header displayed above a given configuration node or value
*
* @param path to the node or value the header is for
* @param header to set to
*/
public void setHeader(String path, String header) {
if (header == null) {
this.headers.remove(this.getPath(path));
} else {
this.headers.put(this.getPath(path), header);
}
}
/**
* Adds a new line to the header displayed above this configuration node
*
* @param header line to add
*/
public void addHeader(String header) {
this.addHeader("", header);
}
/**
* Adds a new line to the header displayed above a given configuration node or value
*
* @param path to the node or value the header is for
* @param header line to add
*/
public void addHeader(String path, String header) {
String oldheader = this.getHeader(path);
if (oldheader == null) {
this.setHeader(path, header);
} else {
this.setHeader(path, oldheader + "\n" + header);
}
}
/**
* Gets all the headers displayed for this Node and the sub-nodes
*
* @return A mapping of keys vs. headers
*/
public Map<String, String> getHeaders() {
String root = this.getPath();
Map<String, String> rval = new HashMap<String, String>(this.headers.size());
if (LogicUtil.nullOrEmpty(root)) {
rval.putAll(this.headers);
} else {
for (Map.Entry<String, String> entry : this.headers.entrySet()) {
if (entry.getKey().startsWith(root)) {
rval.put(entry.getKey(), entry.getValue());
}
}
}
return rval;
}
/**
* Clears all the headers displayed for this node and the values and sub-nodes in this node
*/
public void clearHeaders() {
String root = this.getPath();
if (root == null || root.length() == 0) {
this.headers.clear();
} else {
Iterator<Map.Entry<String, String>> iter = this.headers.entrySet().iterator();
while (iter.hasNext()) {
if (iter.next().getKey().startsWith(root)) {
iter.remove();
}
}
}
}
/**
* Gets the Memory Section represented by this Configuration Node
*
* @return Memory Section of this Node
*/
public MemorySection getSection() {
return this.source;
}
/**
* Gets the YamlConfiguration root source for this Configuration
*
* @return Yaml Configuration root
*/
public YamlConfiguration getSource() {
return (YamlConfiguration) this.source.getRoot();
}
/**
* Checks if a node is contained at the path specified
*
* @param path to check at
* @return True if it is a node, False if not
*/
public boolean isNode(String path) {
return this.source.isConfigurationSection(path);
}
/**
* Gets the node at the path specified, creates one if not present
*
* @param path to get a node
* @return the node
*/
public ConfigurationNode getNode(String path) {
return new ConfigurationNode(this, path);
}
/**
* Gets all configuration nodes
*
* @return Set of configuration nodes
*/
public Set<ConfigurationNode> getNodes() {
Set<ConfigurationNode> rval = new HashSet<ConfigurationNode>();
for (String path : this.getKeys()) {
if (this.isNode(path)) {
rval.add(this.getNode(path));
}
}
return rval;
}
/**
* Gets all the values mapped to the keys
*
* @return map of the values
*/
public Map<String, Object> getValues() {
return this.source.getValues(false);
}
/**
* Gets all the values mapped to the keys of the type specified
*
* @param type to convert to
* @return map of the converted values
*/
public <T> Map<String, T> getValues(Class<T> type) {
Map<String, Object> values = this.getValues();
Iterator<Map.Entry<String, Object>> iter = values.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, Object> entry = iter.next();
T newvalue = ParseUtil.convert(entry.getValue(), type);
if (newvalue == null) {
iter.remove();
} else {
entry.setValue(newvalue);
}
}
return (Map<String, T>) values;
}
/**
* Gets all the keys of the values
*
* @return key set
*/
public Set<String> getKeys() {
return this.source.getKeys(false);
}
/**
* Clones this ConfigurationNode in a way that it no longer references the backing file configuration
*/
@Override
public ConfigurationNode clone() {
ConfigurationNode cloned = new ConfigurationNode();
cloned.setHeader(this.getHeader());
for (String key : this.getKeys()) {
if (this.isNode(key)) {
cloned.set(key, this.getNode(key).clone());
} else {
cloned.set(key, this.get(key));
}
}
return cloned;
}
public boolean isRead() {
return this.isRead(null);
}
public boolean isRead(String path) {
return this.readkeys.contains(this.getPath(path));
}
public void setRead() {
this.setRead(null);
}
public void setRead(String path) {
this.setReadFullPath(this.getPath(path));
}
private void setReadFullPath(String path) {
if (this.readkeys.add(path)) {
int dotindex = path.lastIndexOf('.');
if (dotindex > 0) {
this.setReadFullPath(path.substring(0, dotindex));
}
}
}
/**
* Trims away all unread values
*/
public void trim() {
for (String key : this.getKeys()) {
if (this.isRead(key)) {
if (this.isNode(key)) {
this.getNode(key).trim();
}
} else {
this.remove(key);
}
}
}
/**
* Checks if a value is contained at the path specified
*
* @param path to check at
* @return True if a value is contained, False if not
*/
public boolean contains(String path) {
return this.source.contains(path);
}
/**
* Clears all values
*/
public void clear() {
for (String key : this.getKeys())
this.remove(key);
}
/**
* Removes the value at the path specified
*
* @param path to remove at
*/
public void remove(String path) {
this.set(path, null);
}
/**
* Sets a value at a certain path
*
* @param path to set
* @param value to set to
*/
public void set(String path, Object value) {
if (value != null) {
this.setRead(path);
if (value.getClass().isEnum()) {
String text = value.toString();
if (text.equals("true")) {
value = true;
} else if (text.equals("false")) {
value = false;
} else {
value = text;
}
}
}
this.source.set(path, value);
}
/**
* Gets the value at the path as a List
*
* @param path to get at
* @return The value as a List
*/
public List getList(String path) {
this.setRead(path);
return this.source.getList(path);
}
/**
* Gets the value at the path as a List of a given type
*
* @param path to get at
* @param type of the list to get
* @return The value as a list of the type
*/
public <T> List<T> getList(String path, Class<T> type) {
return this.getList(path, type, new ArrayList<T>());
}
/**
* Gets the value at the path as a List of a given type
*
* @param path to get at
* @param type of the list to get
* @param def list to return if not contained
* @return The value as a list of the type
*/
public <T> List<T> getList(String path, Class<T> type, List<T> def) {
List list = this.getList(path);
if (list != null) {
def = new ArrayList<T>();
T val;
for (Object o : list) {
val = ParseUtil.convert(o, type);
if (val != null)
def.add(val);
}
}
this.set(path, def);
return def;
}
/**
* Gets the raw value at the path specified
*
* @param path to get at
* @return the raw value
*/
public Object get(String path) {
this.setRead(path);
return this.source.get(path);
}
/**
* Gets the raw value at the path as the type specified
*
* @param path to get at
* @param type of value to get
* @return the converted value, or null if not found or invalid
*/
public <T> T get(String path, Class<T> type) {
return this.get(path, type, null);
}
/**
* Gets the raw value at the path as the type specified<br>
* <b>The def value is used to get the type, it can not be null!</b>
*
* @param path to get at
* @param def value to return
* @return the converted value, or the default value if not found or invalid
*/
public <T> T get(String path, T def) {
return this.get(path, (Class<T>) def.getClass(), def);
}
/**
* Gets the raw value at the path as the type specified
*
* @param path to get at
* @param type of value to get
* @param def value to return
* @return the converted value, or the default value if not found or invalid
*/
public <T> T get(String path, Class<T> type, T def) {
Object rawValue = this.get(path);
if (type == String.class && rawValue instanceof String[]) {
// Special conversion to line-by-line String
// This is needed, as it saves line-split Strings as such
return (T) StringUtil.join("\n", (String[]) rawValue);
}
T rval = ParseUtil.convert(this.get(path), type, def);
this.set(path, rval);
return rval;
}
/**
* Shares a single value with a target collection:<br>
* - Writes the value from this node to the target if possible<br>
* - Writes the value from the target to this node alternatively<br>
* - If no value was found at all, both the target and this node get the default value
*
* @param target to share the value with
* @param path to the value to share
* @param def value to use if no value was found
*/
public void shareWith(Map<String, Object> target, String path, Object def) {
Object value = this.get(path);
if (value != null) {
target.put(path, value);
} else {
value = target.get(path);
if (value == null) {
value = def;
target.put(path, value);
}
this.set(path, value);
}
}
}