/* * 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.observer.agent.AgentMultimap; import com.jcwhatever.nucleus.collections.observer.agent.AgentSetMultimap; import com.jcwhatever.nucleus.internal.NucMsg; 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.items.ItemStackUtils; import com.jcwhatever.nucleus.utils.observer.future.FutureAgent; import com.jcwhatever.nucleus.utils.observer.future.IFuture; import com.jcwhatever.nucleus.utils.observer.future.IFuture.FutureStatus; import org.bukkit.Location; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.MemorySection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; /** * A YAML based data node. */ public class YamlDataNode extends AbstractDataNode { /** * Convert a {@link DataPath} instance to a {@link java.io.File} which * points to a disk based YAML 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] + ".yml"); } private final Plugin _plugin; private final YamlDataNode _root; // instantiated on root only private final ConfigurationSection _section; private final Map<String, YamlDataNode> _cachedNodes; private final AgentMultimap<IDataNode, FutureAgent> _saveAgents; private volatile boolean _isLoaded; private volatile IScheduledTask _saveTask; protected String _yamlString; protected File _file; /** * Constructor. * * @param plugin The owning plugin. * @param storagePath The storage path. */ public YamlDataNode(Plugin plugin, DataPath storagePath) { this(plugin); PreCon.notNull(storagePath); _file = dataPathToFile(plugin, storagePath); if (!_file.exists()) { try { if (!_file.createNewFile()) { throw new RuntimeException("Failed to crate initial Yaml file."); } } catch (IOException e) { e.printStackTrace(); } } } /** * Constructor. * * @param plugin The owning plugin. * @param file The yaml file. */ public YamlDataNode(Plugin plugin, File file) { this(plugin); _file = file; } /** * Constructor. * * @param plugin The owning plugin. * @param yamlString The yaml string. */ public YamlDataNode(Plugin plugin, String yamlString) { this(plugin); _yamlString = yamlString; try { ((YamlConfiguration)_section).loadFromString(yamlString); _isLoaded = true; } catch (InvalidConfigurationException e) { e.printStackTrace(); } } /** * Private Constructor. Used by public constructors * for root node. * * @param plugin The owning plugin. */ private YamlDataNode(Plugin plugin) { PreCon.notNull(plugin); _plugin = plugin; YamlConfiguration yaml = new YamlConfiguration(); yaml.options().indent(2); _section = yaml; _root = this; _saveAgents = new AgentSetMultimap<>(); _cachedNodes = new HashMap<>(10); } /** * Private Constructor. Used for creating child nodes. * * @param root The root node. * @param path The full node path. */ private YamlDataNode(YamlDataNode root, String path) { super(root, path); _root = root; _section = null; _plugin = root.getPlugin(); _saveAgents = null; _cachedNodes = null; } @Override public Plugin getPlugin() { return _plugin; } @Override public YamlDataNode getRoot() { return _root; } @Override public boolean isRoot() { return _root == this; } @Override public boolean isLoaded() { return getRoot()._isLoaded; } @Override public boolean load() { if (getRoot()._file == null && getRoot()._isLoaded) return true; if (getRoot()._file == null) return false; if (!getRoot()._file.exists()) return false; YamlConfiguration yaml = (YamlConfiguration)getRoot()._section; getRoot()._write.lock(); try { yaml.load(getRoot()._file); return getRoot()._isLoaded = true; } catch (Exception e) { e.printStackTrace(); } finally { getRoot()._write.unlock(); } if (getRoot()._file != null) NucMsg.severe("The config-file '{0}' failed to load.", getRoot()._file.getName()); return getRoot()._isLoaded = false; } @Override public IFuture loadAsync() { final FutureAgent agent = new FutureAgent(); Scheduler.runTaskLaterAsync(_plugin, 1, new Runnable() { @Override public void run() { getRoot()._write.lock(); try { boolean isLoaded = load(); agent.sendStatus( isLoaded ? FutureStatus.SUCCESS : FutureStatus.ERROR, null ); } finally { getRoot()._write.unlock(); } } }); return agent.getFuture(); } @Override public boolean saveSync() { if (!isRoot()) { //noinspection TailRecursion return getRoot().saveSync(); } YamlConfiguration yaml = (YamlConfiguration)_section; boolean isSaved; _write.lock(); try { try { // save yaml if (_file != null) { yaml.save(_file); } else { _yamlString = yaml.getKeys(false).size() == 0 ? "" : yaml.saveToString(); } isSaved = true; // mark dirty nodes as clean cleanAll(); } catch (Exception e) { e.printStackTrace(); isSaved = false; } } finally { _write.unlock(); } return isSaved; } @Override public IFuture save() { final FutureAgent agent = new FutureAgent(); getRoot()._saveAgents.put(this, agent); // check that 1 or more batch operations are not in progress. if (getRoot()._saveTask != null) { return agent.getFuture(); } if (_plugin.isEnabled()) { getRoot()._saveTask = Scheduler.runTaskLaterAsync(_plugin, 1, new Runnable() { @Override public void run() { final boolean isSaved = saveSync(); final Collection<FutureAgent> agents = getRoot()._saveAgents.removeAll(YamlDataNode.this); getRoot()._saveTask = null; if (agents.isEmpty()) return; // return results on main thread Scheduler.runTaskSync(_plugin, new Runnable() { @Override public void run() { for (FutureAgent agent : agents) { if (isSaved) agent.success(); else agent.error(); } } }); } }); } else { if (saveSync()) { agent.success(); } else { agent.error(); } } return agent.getFuture(); } @Override public boolean saveSync(File destination) { getRoot()._write.lock(); try { try { getYamlConfiguration().save(destination); } catch (IOException e) { e.printStackTrace(); } } finally { getRoot()._write.unlock(); } return true; } @Override public IFuture save(final File destination) { final FutureAgent agent = new FutureAgent(); // save on alternate thread Scheduler.runTaskLaterAsync(_plugin, 1, new Runnable() { @Override public void run() { final boolean isSaved = saveSync(destination); if (!agent.hasSubscribers()) return; // return results on main thread Scheduler.runTaskSync(_plugin, new Runnable() { @Override public void run() { agent.sendStatus( isSaved ? FutureStatus.SUCCESS : FutureStatus.ERROR, null); } }); } }); return agent.getFuture(); } @Override public AutoSaveMode getDefaultAutoSaveMode() { return AutoSaveMode.DISABLED; } @Override public int size() { if (_section == null) { //noinspection TailRecursion return getRoot().size(getFullPath("")); } getRoot()._read.lock(); try { return _section.getKeys(false).size(); } finally { getRoot()._read.unlock(); } } public int size(String nodePath) { if (_section == null) { //noinspection TailRecursion return getRoot().size(getFullPath(nodePath)); } getRoot()._read.lock(); try { ConfigurationSection section = _section.getConfigurationSection(nodePath); if (section == null) return 0; return section.getKeys(false).size(); } finally { getRoot()._read.unlock(); } } @Override public Collection<String> getSubNodeNames() { return getSubNodeNames("", new HashSet<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 HashSet<String>(0)); } @Override public <T extends Collection<String>> T getSubNodeNames(String nodePath, T output) { PreCon.notNull(nodePath); PreCon.notNull(output); if (_section == null) { //noinspection TailRecursion return getRoot().getSubNodeNames(getFullPath(nodePath), output); } getRoot()._read.lock(); try { if (_section.get(nodePath) == null) return output; ConfigurationSection section = _section.getConfigurationSection(nodePath); if (section == null) return output; output.addAll(section.getKeys(false)); return output; } finally { getRoot()._read.unlock(); } } @Override public Object get(String keyPath) { if (_section == null) { //noinspection TailRecursion return getRoot().get(getFullPath(keyPath)); } getRoot()._read.lock(); try { return _section.get(keyPath); } finally { getRoot()._read.unlock(); } } @Override public boolean set(String keyPath, @Nullable Object value) { markDirty(); if (_section == null) { //noinspection TailRecursion if (getRoot().set(getFullPath(keyPath), value)) { return true; } return false; } getRoot()._write.lock(); try { 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, 3); } 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 IDataNodeSerializable) { IDataNodeSerializable serializable = (IDataNodeSerializable)value; IDataNode dataNode = getNode(keyPath); dataNode.clear(); serializable.serialize(dataNode); return true; } else if (value instanceof CharSequence) { value = value.toString(); } _section.set(keyPath, value); } finally { getRoot()._write.unlock(); } return true; } @Override public Map<String, Object> getAllValues() { return getAllValues(""); } public Map<String, Object> getAllValues(String nodePath) { getRoot()._read.lock(); try { ConfigurationSection subSection = getRoot()._section.getConfigurationSection(getFullPath(nodePath)); @SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) (subSection == null ? Collections.emptyMap() : subSection.getValues(true)); Iterator<Entry<String, Object>> iterator = result.entrySet().iterator(); while (iterator.hasNext()) { Entry<String, Object> entry = iterator.next(); if (entry.getValue() instanceof MemorySection) iterator.remove(); } return result; } finally { getRoot()._read.unlock(); } } @Override public void remove() { if (getRoot() == this) throw new UnsupportedOperationException("Cannot remove the root node."); getRoot().remove(getFullPath("")); } @Override public void remove(String nodePath) { if (getRoot() == this && nodePath.isEmpty()) throw new UnsupportedOperationException("Cannot remove the root node."); set(nodePath, null); } @Override public boolean hasNode(String nodePath) { return get(nodePath) != null; } @Override public IDataNode getNode(String nodePath) { PreCon.notNull(nodePath); String fullPath = getFullPath(nodePath); if (fullPath.isEmpty()) return getRoot(); YamlDataNode node = getRoot()._cachedNodes.get(fullPath); if (node == null) { node = new YamlDataNode(getRoot(), fullPath); getRoot()._cachedNodes.put(fullPath, node); } return node; } @Override public void clear() { if (isRoot()) { Collection<String> keys = getSubNodeNames(); for (String key : keys) { set(key, null); } } else { getRoot().set(getFullPath(""), null); } } public YamlConfiguration getYamlConfiguration() { return (YamlConfiguration)getRoot()._section; } }