com.jcwhatever.nucleus.storage.JsonDataNode.java Source code

Java tutorial

Introduction

Here is the source code for com.jcwhatever.nucleus.storage.JsonDataNode.java

Source

/*
 * 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]);
    }
}