/*
* This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT).
*
* Copyright (c) JCThePants (www.jcwhatever.com)
*
* 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.jcwhatever.nucleus.storage;
import com.jcwhatever.nucleus.collections.wrap.ConversionIteratorWrapper;
import com.jcwhatever.nucleus.managed.items.serializer.InvalidItemStackStringException;
import com.jcwhatever.nucleus.managed.scheduler.Scheduler;
import com.jcwhatever.nucleus.storage.serialize.DeserializeException;
import com.jcwhatever.nucleus.storage.serialize.IDataNodeSerializable;
import com.jcwhatever.nucleus.utils.ArrayUtils;
import com.jcwhatever.nucleus.utils.EnumUtils;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.Rand;
import com.jcwhatever.nucleus.utils.coords.LocationUtils;
import com.jcwhatever.nucleus.utils.coords.SyncLocation;
import com.jcwhatever.nucleus.utils.items.ItemStackUtils;
import com.jcwhatever.nucleus.utils.text.TextUtils;
import org.bukkit.Location;
import org.bukkit.configuration.MemorySection;
import org.bukkit.inventory.ItemStack;
import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
/**
* Abstract implementation of an {@link IDataNode}.
*/
public abstract class AbstractDataNode implements IDataNode {
private static final Map<AbstractDataNode, Void> _autoSaveNodes = new WeakHashMap<>(25);
private static AutoSaveRunner _autoSaveRunner;
private final AbstractDataNode _root;
private final String _parentPath;
private volatile AbstractDataNode _parent;
private volatile AutoSaveMode _saveMode = AutoSaveMode.DEFAULT;
private volatile boolean _isDirty;
private volatile int _dirtyChildren;
private volatile Boolean _isDefaultSaved;
protected final String _rawPath;
protected final String _path;
protected final String _nodeName;
// instantiated on root only
protected final ReadLock _read;
protected final WriteLock _write;
protected final Set<AbstractDataNode> _dirtyNodes;
/**
* Constructor for the root node.
*/
protected AbstractDataNode() {
_rawPath = "";
_path = "";
_nodeName = "";
_root = this;
_parentPath = null;
_dirtyNodes = new HashSet<>(5);
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
_read = lock.readLock();
_write = lock.writeLock();
}
/**
* Constructor for child nodes.
*
* @param path The data node path.
*/
protected AbstractDataNode(AbstractDataNode root, String path) {
if (path.endsWith(".")) {
_rawPath = path.substring(0, path.length() - 1);
_path = path;
} else {
_rawPath = path;
_path = path + '.';
}
String[] pathcomp = TextUtils.PATTERN_DOT.split(_rawPath);
_nodeName = pathcomp.length > 0
? pathcomp[pathcomp.length - 1]
: "";
_read = null;
_write = null;
_root = root;
_parentPath = getParentPath(path);
_dirtyNodes = null;
}
@Override
public String getName() {
return _nodeName;
}
@Override
public String getNodePath() {
return _rawPath;
}
@Nullable
@Override
public IDataNode getParent() {
if (_parentPath == null)
return null;
if (_parent == null) {
if (_parent != null)
return _parent;
_parent = _parentPath.isEmpty()
? (AbstractDataNode) getRoot()
: (AbstractDataNode) getRoot().getNode(_parentPath);
}
return _parent;
}
@Override
public boolean isDirty() {
return _isDirty || _dirtyChildren > 0;
}
@Override
public AutoSaveMode getAutoSaveMode() {
return _saveMode;
}
@Override
public void setAutoSaveMode(AutoSaveMode mode) {
PreCon.notNull(mode);
if (_saveMode == mode)
return;
_saveMode = mode;
}
@Override
public boolean isDefaultsSaved() {
Boolean isSaved;
AbstractDataNode node = this;
while (node != null) {
isSaved = node._isDefaultSaved;
if (isSaved != null)
return isSaved;
if (node == _root)
break;
node = (AbstractDataNode) node.getParent();
}
return false;
}
@Override
public void setDefaultsSaved(boolean isSaved) {
_isDefaultSaved = isSaved;
}
@Override
public boolean getBoolean(String keyPath) {
return getBoolean(keyPath, false);
}
@Override
public boolean getBoolean(String keyPath, boolean def) {
Object value = getBooleanObject(keyPath);
if (value instanceof Boolean) {
return (Boolean) value;
}
else if (value instanceof String) {
return TextUtils.parseBoolean((String) value);
}
if (isDefaultsSaved())
set(keyPath, def);
return def;
}
@Override
public int getInteger(String keyPath) {
return getInteger(keyPath, 0);
}
@Override
public int getInteger(String keyPath, int def) {
Object value = getNumberObject(keyPath);
if (value instanceof Number) {
return ((Number) value).intValue();
}
else if (value instanceof String) {
return TextUtils.parseInt((String)value, def);
}
if (isDefaultsSaved())
set(keyPath, def);
return def;
}
@Override
public long getLong(String keyPath) {
return getLong(keyPath, 0);
}
@Override
public long getLong(String keyPath, long def) {
Object value = getNumberObject(keyPath);
if (value instanceof Number) {
return ((Number) value).longValue();
}
else if (value instanceof String) {
return TextUtils.parseLong((String)value, def);
}
if (isDefaultsSaved())
set(keyPath, def);
return def;
}
@Override
public double getDouble(String keyPath) {
return getDouble(keyPath, 0.0D);
}
@Override
public double getDouble(String keyPath, double def) {
Object value = getNumberObject(keyPath);
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
else if (value instanceof String) {
return TextUtils.parseDouble((String)value, def);
}
if (isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public String getString(String keyPath) {
return getString(keyPath, null);
}
@Nullable
@Override
public String getString(String keyPath, @Nullable String def) {
Object value = getStringObject(keyPath);
if (value instanceof MemorySection)
return def;
if (value != null)
return String.valueOf(value);
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public UUID getUUID(String keyPath) {
return getUUID(keyPath, null);
}
@Nullable
@Override
public UUID getUUID(String keyPath, @Nullable UUID def) {
Object value = getStringObject(keyPath);
if (value instanceof UUID) {
return (UUID) value;
}
else if (value instanceof String) {
UUID result = TextUtils.parseUUID((String)value);
if (result != null)
return result;
}
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public Date getDate(String keyPath) {
return getDate(keyPath, null);
}
@Nullable
@Override
public Date getDate(String keyPath, @Nullable Date def) {
Object value = getNumberObject(keyPath);
if (value instanceof Date) {
return (Date) value;
}
else if (value instanceof Number) {
return new Date(((Number)value).longValue());
}
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public SyncLocation getLocation(String keyPath) {
return getLocation(keyPath, null);
}
@Nullable
@Override
public SyncLocation getLocation(String keyPath, @Nullable Location def) {
Object value = getStringObject(keyPath);
if (value instanceof Location) {
return new SyncLocation((Location) value);
}
else if (value instanceof String) {
SyncLocation location = LocationUtils.parseLocation((String) value);
if (location != null)
return location;
}
if (def == null)
return null;
if (isDefaultsSaved())
set(keyPath, def);
if (def instanceof SyncLocation)
return (SyncLocation)def;
return new SyncLocation(def);
}
@Nullable
@Override
public ItemStack[] getItemStacks(String keyPath) {
return getItemStacks(keyPath, (ItemStack[])null);
}
@Nullable
@Override
public ItemStack[] getItemStacks(String keyPath, @Nullable ItemStack def) {
return getItemStacks(keyPath, def != null ? new ItemStack[]{def} : null);
}
@Nullable
@Override
public ItemStack[] getItemStacks(String keyPath, @Nullable ItemStack[] def) {
Object value = getStringObject(keyPath);
if (value instanceof ItemStack) {
return new ItemStack[]{(ItemStack) value};
}
else if (value instanceof ItemStack[]) {
return ((ItemStack[])value).clone();
}
else if (value instanceof String) {
try {
String str = (String)value;
if (str.indexOf("%ItemStack[]% ") == 0) {
str = str.substring(14);
}
return ItemStackUtils.parse(str);
} catch (InvalidItemStackStringException ignore) {}
}
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public <T extends Enum<T>> T getEnum(String keyPath, Class<T> enumClass) {
return getEnum(keyPath, null, enumClass);
}
@Nullable
@Override
public <T extends Enum<T>> T getEnum(String keyPath, @Nullable T def, Class<T> enumClass) {
Object value = getStringObject(keyPath);
if (enumClass.isInstance(value)) {
@SuppressWarnings("unchecked")
T result = (T)value;
return result;
}
else if (value instanceof String) {
T result = EnumUtils.searchEnum((String) value, enumClass);
if (result != null)
return result;
}
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public Enum<?> getEnumGeneric(String keyPath, @Nullable Enum<?> def, Class<? extends Enum<?>> enumClass) {
Object value = getStringObject(keyPath);
if (enumClass.isInstance(value)) {
@SuppressWarnings("unchecked")
Enum<?> result = (Enum<?>)value;
return result;
}
else if (value instanceof String) {
return EnumUtils.searchGenericEnum((String) value, enumClass, def);
}
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public List<String> getStringList(String keyPath, @Nullable List<String> def) {
Object value = getCollectionObject(keyPath);
if (value instanceof Collection) {
Collection collection = (Collection)value;
List<String> result = new ArrayList<>(collection.size());
for (Object obj : collection) {
result.add(String.valueOf(obj));
}
return result;
}
else if (value instanceof String[]) {
String[] source = (String[])value;
List<String> result = new ArrayList<>(source.length);
Collections.addAll(result, source);
return result;
}
else if (value instanceof String) {
List<String> result = new ArrayList<>(5);
result.add(String.valueOf(value));
return result;
}
if (def != null && isDefaultsSaved())
set(keyPath, def);
return def;
}
@Nullable
@Override
public <T extends IDataNodeSerializable> T getSerializable(String nodePath, Class<T> typeClass) {
PreCon.notNull(nodePath);
PreCon.notNull(typeClass);
if (!hasNode(nodePath))
return null;
T instance;
try {
Constructor<T> constructor = typeClass.getDeclaredConstructor();
constructor.setAccessible(true);
instance = constructor.newInstance();
} catch (InstantiationException | NoSuchMethodException |
InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException("Failed to instantiate IDataNodeSerializable object. " +
"Make sure it has an empty constructor.");
}
IDataNode dataNode = getNode(nodePath);
_root._read.lock();
try {
try {
instance.deserialize(dataNode);
} catch (DeserializeException ignore) {
return null;
}
}
finally {
_root._read.unlock();
}
return instance;
}
@Override
public Iterator<IDataNode> iterator() {
return new ConversionIteratorWrapper<IDataNode, String>() {
Iterator<String> iterator = getSubNodeNames().iterator();
@Override
protected IDataNode convert(String nodeName) {
return getNode(nodeName);
}
@Override
protected Iterator<String> iterator() {
return iterator;
}
@Override
public void remove() {
_root._write.lock();
try {
iterator.remove();
}
finally {
_root._write.unlock();
}
}
};
}
/**
* Retrieve the value of key path where the value is
* expected to be a boolean.
*
* @param keyPath The key path.
*/
@Nullable
protected Object getBooleanObject(String keyPath) {
return get(keyPath);
}
/**
* Retrieve the value of a key path where the value is
* expected to be a number.
*
* @param keyPath The key path.
*/
@Nullable
protected Object getNumberObject(String keyPath) {
return get(keyPath);
}
/**
* Retrieve the value of a key path where the value is
* expected to be a string.
*
* @param keyPath The key path.
*/
@Nullable
protected Object getStringObject(String keyPath) {
return get(keyPath);
}
/**
* Retrieve the value of a key path where the value is
* expected to be a collection.
*
* @param keyPath The key path.
*/
@Nullable
protected Object getCollectionObject(String keyPath) {
return get(keyPath);
}
/**
* Get the full path from the root node of the
* specified path relative to the current node.
*
* @param relativePath The relative path.
*/
protected String getFullPath(String relativePath) {
if (relativePath.isEmpty())
return _rawPath;
return _path + relativePath;
}
/**
* Get the full path from the root node to the parent of the specified child node.
*
* @param fullPath The full path to the child node.
*
* @return The full path to parent or null if the current node is root.
*/
@Nullable
protected String getParentPath(String fullPath) {
if (fullPath.isEmpty()) {
return null;
}
String[] components = TextUtils.PATTERN_DOT.split(fullPath);
if (components.length == 1)
return "";
components = ArrayUtils.reduceEnd(components, 1);
return TextUtils.concat(components, ".");
}
/**
* To be invoked by implementation to mark the node
* as modified without saving.
*/
protected void markDirty() {
// don't mark again if already marked.
if (_isDirty)
return;
_isDirty = true;
_root._dirtyNodes.add(this);
parentDirty();
if (_saveMode == AutoSaveMode.ENABLED) {
synchronized (_autoSaveNodes) {
_autoSaveNodes.put(this, null);
if (_autoSaveRunner == null) {
_autoSaveRunner = new AutoSaveRunner();
Scheduler.runTaskRepeatAsync(getPlugin(),
Rand.getInt(1, 40), 40, _autoSaveRunner);
}
}
}
}
/**
* To be invoked from implementation upon saving the node.
*/
protected void clean() {
_root._dirtyNodes.remove(this);
safeClean();
}
/**
* To be invoked from implementation to indicate all nodes are saved.
*/
protected void cleanAll() {
for (AbstractDataNode node : _root._dirtyNodes) {
node.safeClean();
}
_root._dirtyNodes.clear();
}
// mark all parents as dirty without adding them
// to the auto save pool.
private void parentDirty() {
AbstractDataNode node = this;
while ((node = (AbstractDataNode)node.getParent()) != null) {
node._dirtyChildren++;
}
}
private void safeClean() {
_isDirty = false;
AbstractDataNode node = this;
while ((node = (AbstractDataNode)node.getParent()) != null) {
node._dirtyChildren--;
}
}
/**
* Auto save runnable.
*/
private static class AutoSaveRunner implements Runnable {
@Override
public void run() {
synchronized (_autoSaveNodes) {
Iterator<AbstractDataNode> iterator = _autoSaveNodes.keySet().iterator();
while (iterator.hasNext()) {
AbstractDataNode dataNode = iterator.next();
if (dataNode.getAutoSaveMode() == AutoSaveMode.ENABLED ||
(dataNode.getAutoSaveMode() == AutoSaveMode.DEFAULT &&
dataNode.getDefaultAutoSaveMode() == AutoSaveMode.ENABLED)) {
dataNode.save();
dataNode.clean();
}
iterator.remove();
}
}
}
}
}