/*
* 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.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.jcwhatever.nucleus.managed.scheduler.IScheduledTask;
import com.jcwhatever.nucleus.managed.scheduler.Scheduler;
import com.jcwhatever.nucleus.storage.serialize.IDataNodeSerializable;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.coords.LocationUtils;
import com.jcwhatever.nucleus.utils.file.FileUtils;
import com.jcwhatever.nucleus.utils.items.ItemStackUtils;
import com.jcwhatever.nucleus.utils.observer.future.FutureAgent;
import com.jcwhatever.nucleus.utils.observer.future.IFuture;
import com.jcwhatever.nucleus.utils.text.TextUtils;
import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* A JSON based data node.
*/
public class JsonDataNode extends AbstractDataNode {
/**
* Convert a {@link DataPath} instance to a {@link java.io.File} which
* points to a disk based JSON file.
*
* @param plugin The owning plugin. Used to determine the base
* path of the file.
* @param dataPath The {@link DataPath} to convert.
*/
public static File dataPathToFile(Plugin plugin, DataPath dataPath) {
String[] pathComp = dataPath.getPath();
if (pathComp.length == 0)
throw new IllegalArgumentException("Storage path cannot be empty.");
File directory = plugin.getDataFolder();
for (int i = 0; i < pathComp.length - 1; i++) {
directory = new File(directory, pathComp[i]);
}
if (!directory.exists() && !directory.mkdirs())
throw new RuntimeException("Failed to create folders corresponding to supplied data path.");
return new File(directory, pathComp[pathComp.length - 1] + ".json");
}
private final Plugin _plugin;
private final Gson _gson;
private JsonObject _object;
private boolean _isLoaded;
private final JsonDataNode _root;
private final Deque<FutureAgent> _saveAgents;
private IScheduledTask _saveTask;
private File _file;
private String _json;
/**
* Constructor.
*
* @param plugin The owning plugin.
* @param file The json file to load from and/or save to.
*/
public JsonDataNode(Plugin plugin, File file) {
_plugin = plugin;
_gson = new GsonBuilder().setPrettyPrinting().create();
_object = null;
_root = this;
_file = file;
_object = _gson.fromJson("{}", JsonObject.class);
_saveAgents = new ArrayDeque<>(5);
}
/**
* Constructor.
*
* @param plugin The owning plugin.
* @param json The json string to load.
*/
public JsonDataNode(Plugin plugin, String json) {
_plugin = plugin;
_gson = new GsonBuilder().setPrettyPrinting().create();
_object = _gson.fromJson(json, JsonObject.class);
_isLoaded = true;
_root = this;
_json = json;
_saveAgents = new ArrayDeque<>(5);
}
/**
* Private constructor.
*
* <p>Used for sub data node.</p>
*
* @param root The root data node.
* @param basePath The sub nodes base path.
*/
private JsonDataNode(JsonDataNode root, String basePath) {
super(root, basePath);
_root = root;
_plugin = root.getPlugin();
_gson = root._gson;
_saveAgents = null;
_isLoaded = true;
}
/**
* Get the saved json text.
*
* @return The json text. Returns all json from root node.
*/
public String getJson() {
return _file == null ? _json : _root._gson.toJson(_root._object);
}
@Override
public Plugin getPlugin() {
return _plugin;
}
@Override
public boolean isLoaded() {
return _isLoaded;
}
@Override
public IDataNode getRoot() {
return _root;
}
@Override
public boolean isRoot() {
return _root == this;
}
@Override
public boolean load() {
if (_file == null)
return true;
if (!_file.exists() || !_file.isFile())
return false;
FileReader reader;
try {
reader = new FileReader(_file);
} catch (FileNotFoundException e) {
e.printStackTrace();
return false;
}
try {
_object = _gson.fromJson(reader, JsonObject.class);
} catch (JsonIOException | JsonSyntaxException e) {
e.printStackTrace();
return false;
}
_isLoaded = true;
return true;
}
@Override
public IFuture loadAsync() {
final FutureAgent agent = new FutureAgent();
Scheduler.runTaskLaterAsync(_plugin, 1, new Runnable() {
@Override
public void run() {
if (load()) {
agent.success();
}
else {
agent.error("The node was not constructed with a file or the file was not found.");
}
}
});
return agent.getFuture();
}
@Override
public boolean saveSync() {
if (_file != null) {
return saveSync(_file);
}
else {
_json = _gson.toJson(_object);
clean();
return true;
}
}
@Override
public IFuture save() {
FutureAgent agent = new FutureAgent();
_root._saveAgents.add(agent);
if (_file == null || !_plugin.isEnabled()) {
return saveSync() ? agent.success() : agent.error("Error while saving.");
}
if (_root._saveTask != null)
return agent.getFuture();
Scheduler.runTaskLaterAsync(_plugin, 2, new Runnable() {
@Override
public void run() {
final boolean isSuccess = saveSync();
_root._saveTask = null;
if (_root._saveAgents.isEmpty())
return;
final List<FutureAgent> agents = new ArrayList<FutureAgent>(_root._saveAgents);
Scheduler.runTaskSync(_plugin, new Runnable() {
@Override
public void run() {
for (FutureAgent agent : agents) {
if (isSuccess) {
agent.success();
} else {
agent.error("Failed to save Json to file.");
}
}
}
});
}
});
return agent.getFuture();
}
@Override
public boolean saveSync(File destination) {
String json = _gson.toJson(_object);
if (destination == _file) {
_json = json;
clean();
}
try {
if (!destination.exists() && !destination.createNewFile()) {
return false;
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
int written = FileUtils.writeTextFile(destination, StandardCharsets.UTF_8, json);
if (written == -1) {
return false;
} else {
clean();
return true;
}
}
@Override
public IFuture save(final File destination) {
final FutureAgent agent = new FutureAgent();
if (_file == null || !_plugin.isEnabled()) {
return saveSync(destination) ? agent.success() : agent.error("Error while saving.");
}
Scheduler.runTaskLaterAsync(_plugin, 1, new Runnable() {
@Override
public void run() {
final boolean isSuccess = saveSync(destination);
Scheduler.runTaskSync(_plugin, new Runnable() {
@Override
public void run() {
if (isSuccess) {
agent.success();
} else {
agent.error("Failed to save Json to file.");
}
}
});
}
});
return agent.getFuture();
}
@Override
public AutoSaveMode getDefaultAutoSaveMode() {
return AutoSaveMode.DISABLED;
}
@Override
public int size() {
JsonElement element = getJsonElement("");
if (element == null || !element.isJsonObject())
return 0;
return element.getAsJsonObject().entrySet().size();
}
@Override
public boolean hasNode(String nodePath) {
return getJsonElement(nodePath) != null;
}
@Override
public IDataNode getNode(String nodePath) {
String fullPath = getFullPath(nodePath);
if (fullPath.isEmpty())
return _root;
return new JsonDataNode(_root, fullPath);
}
@Override
public Collection<String> getSubNodeNames() {
return getSubNodeNames("", new ArrayList<String>(0));
}
@Override
public <T extends Collection<String>> T getSubNodeNames(T output) {
return getSubNodeNames("", output);
}
@Override
public Collection<String> getSubNodeNames(String nodePath) {
return getSubNodeNames(nodePath, new ArrayList<String>(0));
}
@Override
public <T extends Collection<String>> T getSubNodeNames(String nodePath, T output) {
JsonElement current = getJsonElement(nodePath);
if (current == null || !current.isJsonObject())
return output;
Set<Map.Entry<String, JsonElement>> entrySet = current.getAsJsonObject().entrySet();
if (output instanceof ArrayList) {
((ArrayList) output).ensureCapacity(entrySet.size() + output.size());
}
for (Map.Entry<String, JsonElement> entry : entrySet) {
output.add(entry.getKey());
}
return output;
}
@Override
public void clear() {
Collection<String> names = getSubNodeNames("");
for (String name : names) {
remove(name);
}
if (names.size() > 0)
markDirty();
}
@Override
public void remove() {
remove("");
}
@Override
public void remove(String nodePath) {
String path = getFullPath(nodePath);
if (path.isEmpty())
throw new UnsupportedOperationException("Cannot remove the root node.");
String[] pathElements = TextUtils.PATTERN_DOT.split(path);
removeKey(pathElements);
}
@Override
public boolean set(String keyPath, @Nullable Object value) {
keyPath = getFullPath(keyPath);
String[] path = TextUtils.PATTERN_DOT.split(keyPath);
if (!keyPath.isEmpty())
removeKey(path);
if (value != null) {
if (value instanceof IDataNodeSerializable) {
IDataNode node = getNode(keyPath);
IDataNodeSerializable serializable = (IDataNodeSerializable)value;
serializable.serialize(node);
markDirty();
return true;
}
if (keyPath.isEmpty())
return false;
if (value instanceof UUID) {
value = String.valueOf(value);
}
else if (value instanceof Date){
value = ((Date) value).getTime();
}
else if (value instanceof Location) {
value = LocationUtils.serialize((Location)value, 2);
}
else if (value instanceof ItemStack) {
value = ItemStackUtils.serialize((ItemStack)value);
}
else if (value instanceof ItemStack[]) {
value = ItemStackUtils.serialize((ItemStack[]) value);
}
else if (value instanceof Enum<?>) {
Enum<?> e = (Enum<?>) value;
value = e.name();
}
else if (value instanceof Collection) {
Collection collection = (Collection)value;
String[] array = new String[collection.size()];
int i=0;
for (Object elm : collection) {
array[i] = elm == null ? null : String.valueOf(elm);
i++;
}
value = array;
}
else if (value.getClass().isArray() && !(value instanceof String[])) {
int size = Array.getLength(value);
String[] array = new String[size];
for (int i=0; i < size; i++) {
Object elm = Array.get(value, i);
array[i] = elm == null ? null : String.valueOf(elm);
}
value = array;
}
else if (value instanceof CharSequence) {
value = value.toString();
}
if (value instanceof String[]) {
String[] array = (String[])value;
JsonArray jsonArray = new JsonArray();
for (String string : array) {
jsonArray.add(_gson.toJsonTree(string));
}
value = jsonArray;
}
addKey(path, value);
}
markDirty();
return true;
}
@Nullable
@Override
public Object get(String keyPath) {
PreCon.notNull(keyPath);
keyPath = getFullPath(keyPath);
if (keyPath.isEmpty())
return _object;
String[] path = TextUtils.PATTERN_DOT.split(keyPath);
JsonElement element = getJsonElement(path);
if (element == null || element.isJsonObject())
return null;
if (element.isJsonArray()) {
return getStringList(keyPath, null);
}
return element.getAsString();
}
@Override
public Map<String, Object> getAllValues() {
Map<String, Object> result = new HashMap<>(50);
JsonElement selfElement = getJsonElement("");
if (selfElement == null)
return result;
if (!selfElement.isJsonObject()) {
result.put("", selfElement.getAsString());
return result;
}
JsonObject object = selfElement.getAsJsonObject();
String basePath = "";
getAllValuesRecursive(object, basePath, result);
return result;
}
@Override
public int hashCode() {
return _root._object.hashCode() + _rawPath.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof JsonDataNode) {
JsonDataNode other = (JsonDataNode)obj;
return other._root._object == _root._object &&
other._rawPath.equals(_rawPath);
}
return false;
}
@Override
@Nullable
protected Object getBooleanObject(String keyPath) {
JsonElement element = getJsonElement(keyPath);
if (element == null || element.isJsonObject())
return null;
return element.getAsBoolean();
}
@Override
@Nullable
protected Object getNumberObject(String keyPath) {
JsonElement element = getJsonElement(keyPath);
if (element == null || element.isJsonObject())
return null;
return element.getAsNumber();
}
@Override
@Nullable
protected Object getStringObject(String keyPath) {
JsonElement element = getJsonElement(keyPath);
if (element == null || element.isJsonObject())
return null;
return element.getAsString();
}
@Override
@Nullable
protected Object getCollectionObject(String keyPath) {
JsonElement element = getJsonElement(keyPath);
if (element == null || element.isJsonObject() || !element.isJsonArray())
return null;
JsonArray array = element.getAsJsonArray();
List<String> result = new ArrayList<>(array.size());
for (int i=0; i < array.size(); i++) {
result.add(array.get(i).getAsString());
}
return result;
}
private void getAllValuesRecursive(JsonObject object, String basePath, Map<String, Object> result) {
Set<Map.Entry<String, JsonElement>> entrySet = object.entrySet();
for (Map.Entry<String, JsonElement> entry : entrySet) {
String name = basePath.isEmpty() ? entry.getKey() : basePath + '.' + entry.getKey();
JsonElement element = entry.getValue();
if (element.isJsonObject()) {
getAllValuesRecursive(element.getAsJsonObject(), name, result);
}
else {
result.put(name, element.getAsString());
}
}
}
// path requires full path
private void removeKey(String[] path) {
if (path.length == 1) {
_root._object.remove(path[0]);
return;
}
JsonElement rootElement = _root._object.get(path[0]);
if (rootElement == null || !rootElement.isJsonObject())
return;
JsonObject object = rootElement.getAsJsonObject();
for (int i=1; i < path.length - 1; i++) {
JsonElement element = object.get(path[i]);
if (element == null || !element.isJsonObject())
return;
object = element.getAsJsonObject();
}
object.remove(path[path.length - 1]);
}
// path requires full path
private void addKey(String[] path, Object value) {
if (path.length == 1) {
_root._object.add(path[0], _gson.toJsonTree(value));
return;
}
JsonElement rootElement = _root._object.get(path[0]);
if (rootElement == null || !rootElement.isJsonObject()) {
_root._object.remove(path[0]);
_root._object.add(path[0], _gson.toJsonTree(_gson.toJsonTree(new Object())));
rootElement = _root._object.get(path[0]);
}
JsonObject object = rootElement.getAsJsonObject();
for (int i=1; i < path.length - 1; i++) {
JsonObject prev = object;
JsonElement element = object.get(path[i]);
if (element == null || !element.isJsonObject()) {
prev.remove(path[i]);
prev.add(path[i], _gson.toJsonTree(_gson.toJsonTree(new Object())));
element = prev.get(path[i]);
}
object = element.getAsJsonObject();
}
if (value instanceof JsonElement) {
object.add(path[path.length - 1], (JsonElement)value);
}
else {
object.add(path[path.length - 1], _gson.toJsonTree(value));
}
}
private JsonElement getJsonElement(String relativePath) {
String fullPath = getFullPath(relativePath);
if (fullPath.isEmpty())
return _root._object;
String[] path = TextUtils.PATTERN_DOT.split(fullPath);
return getJsonElement(path);
}
// path requires full path
private JsonElement getJsonElement(String[] path) {
if (path.length == 1) {
return _root._object.get(path[0]);
}
JsonElement rootElement = _root._object.get(path[0]);
if (rootElement == null || !rootElement.isJsonObject())
return null;
JsonObject object = rootElement.getAsJsonObject();
for (int i=1; i < path.length - 1; i++) {
JsonElement element = object.get(path[i]);
if (element == null || !element.isJsonObject())
return null;
object = element.getAsJsonObject();
}
return object.get(path[path.length - 1]);
}
}