From 328353d05314aa0ca7c6f05aeb1e3bab58e56a40 Mon Sep 17 00:00:00 2001 From: Luck Date: Fri, 20 Apr 2018 21:59:32 +0100 Subject: [PATCH] Implement the option to combine yaml/json/hocon storage files into one --- .../luckperms/bukkit/LPBukkitBootstrap.java | 8 +- .../luckperms/bungee/LPBungeeBootstrap.java | 8 +- .../common/commands/misc/ExportCommand.java | 13 +- .../common/commands/misc/ImportCommand.java | 11 +- .../luckperms/common/config/ContextsFile.java | 23 +- .../dependencies/DependencyManager.java | 46 +- .../dependencies/DependencyRegistry.java | 5 +- .../classloader/PluginClassLoader.java | 4 +- .../classloader/ReflectionClassLoader.java | 6 +- .../relocation/RelocationHandler.java | 5 +- .../common/locale/LocaleManager.java | 6 +- .../common/locale/SimpleLocaleManager.java | 10 +- .../plugin/AbstractLuckPermsPlugin.java | 11 +- .../plugin/bootstrap/LuckPermsBootstrap.java | 6 +- .../common/storage/StorageFactory.java | 27 +- .../luckperms/common/storage/StorageType.java | 12 +- .../common/storage/dao/AbstractDao.java | 2 +- .../common/storage/dao/SplitStorageDao.java | 2 +- ...teDao.java => AbstractConfigurateDao.java} | 430 ++++-------------- .../dao/file/CombinedConfigurateDao.java | 372 +++++++++++++++ .../storage/dao/file/FileActionLogger.java | 6 +- .../storage/dao/file/FileUuidCache.java | 12 +- .../common/storage/dao/file/FileWatcher.java | 178 ++++---- .../dao/file/SeparatedConfigurateDao.java | 366 +++++++++++++++ .../dao/file/loader/ConfigurateLoader.java | 40 ++ .../HoconLoader.java} | 11 +- .../{JsonDao.java => loader/JsonLoader.java} | 11 +- .../{YamlDao.java => loader/YamlLoader.java} | 11 +- .../common/storage/dao/sql/SqlDao.java | 85 ++-- .../file/FlatfileConnectionFactory.java | 23 +- .../connection/file/H2ConnectionFactory.java | 22 +- .../file/SQLiteConnectionFactory.java | 18 +- .../FileUtils.java => utils/MoreFiles.java} | 41 +- .../luckperms/nukkit/LPNukkitBootstrap.java | 8 +- .../luckperms/sponge/LPSpongeBootstrap.java | 11 +- .../sponge/service/LuckPermsService.java | 3 +- .../service/persisted/SubjectStorage.java | 98 ++-- 37 files changed, 1248 insertions(+), 703 deletions(-) rename common/src/main/java/me/lucko/luckperms/common/storage/dao/file/{ConfigurateDao.java => AbstractConfigurateDao.java} (62%) create mode 100644 common/src/main/java/me/lucko/luckperms/common/storage/dao/file/CombinedConfigurateDao.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/storage/dao/file/SeparatedConfigurateDao.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/ConfigurateLoader.java rename common/src/main/java/me/lucko/luckperms/common/storage/dao/file/{HoconDao.java => loader/HoconLoader.java} (82%) rename common/src/main/java/me/lucko/luckperms/common/storage/dao/file/{JsonDao.java => loader/JsonLoader.java} (83%) rename common/src/main/java/me/lucko/luckperms/common/storage/dao/file/{YamlDao.java => loader/YamlLoader.java} (83%) rename common/src/main/java/me/lucko/luckperms/common/{storage/dao/file/FileUtils.java => utils/MoreFiles.java} (57%) diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java index 0f6b7909..ecd950c2 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java @@ -36,15 +36,13 @@ import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; import java.io.InputStream; +import java.nio.file.Path; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; -import javax.annotation.Nullable; - /** * Bootstrap plugin for LuckPerms running on Bukkit. */ @@ -197,8 +195,8 @@ public class LPBukkitBootstrap extends JavaPlugin implements LuckPermsBootstrap } @Override - public File getDataDirectory() { - return getDataFolder(); + public Path getDataDirectory() { + return getDataFolder().toPath().toAbsolutePath(); } @Override diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java index 2fb1840d..e07adaf8 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java @@ -35,15 +35,13 @@ import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Plugin; -import java.io.File; import java.io.InputStream; +import java.nio.file.Path; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; -import javax.annotation.Nullable; - /** * Bootstrap plugin for LuckPerms running on BungeeCord. */ @@ -157,8 +155,8 @@ public class LPBungeeBootstrap extends Plugin implements LuckPermsBootstrap { } @Override - public File getDataDirectory() { - return getDataFolder(); + public Path getDataDirectory() { + return getDataFolder().toPath().toAbsolutePath(); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java index e1f238ea..12ab09bc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java @@ -36,7 +36,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.utils.Predicates; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -57,16 +56,14 @@ public class ExportCommand extends SingleCommand { return CommandResult.STATE_ERROR; } - File f = new File(plugin.getBootstrap().getDataDirectory(), args.get(0)); - if (f.exists()) { - Message.LOG_EXPORT_ALREADY_EXISTS.send(sender, f.getAbsolutePath()); + Path path = plugin.getBootstrap().getDataDirectory().resolve(args.get(0)); + if (Files.exists(path)) { + Message.LOG_EXPORT_ALREADY_EXISTS.send(sender, path.toString()); return CommandResult.INVALID_ARGS; } - Path path = f.toPath(); - try { - f.createNewFile(); + Files.createFile(path); } catch (IOException e) { Message.LOG_EXPORT_FAILURE.send(sender); e.printStackTrace(); @@ -74,7 +71,7 @@ public class ExportCommand extends SingleCommand { } if (!Files.isWritable(path)) { - Message.LOG_EXPORT_NOT_WRITABLE.send(sender, f.getAbsolutePath()); + Message.LOG_EXPORT_NOT_WRITABLE.send(sender, path.toString()); return CommandResult.FAILURE; } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java index 162d3068..03d507c3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java @@ -36,7 +36,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.utils.Predicates; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -58,16 +57,14 @@ public class ImportCommand extends SingleCommand { return CommandResult.STATE_ERROR; } - File f = new File(plugin.getBootstrap().getDataDirectory(), args.get(0)); - if (!f.exists()) { - Message.IMPORT_LOG_DOESNT_EXIST.send(sender, f.getAbsolutePath()); + Path path = plugin.getBootstrap().getDataDirectory().resolve(args.get(0)); + if (!Files.exists(path)) { + Message.IMPORT_LOG_DOESNT_EXIST.send(sender, path.toString()); return CommandResult.INVALID_ARGS; } - Path path = f.toPath(); - if (!Files.isReadable(path)) { - Message.IMPORT_LOG_NOT_READABLE.send(sender, f.getAbsolutePath()); + Message.IMPORT_LOG_NOT_READABLE.send(sender, path.toString()); return CommandResult.FAILURE; } diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java b/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java index 4ca318a7..d8263798 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java @@ -34,10 +34,10 @@ import me.lucko.luckperms.common.contexts.ContextSetJsonSerializer; import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; /** * A wrapper for the 'contexts.json' file. @@ -53,19 +53,23 @@ public class ContextsFile { } public void load() { - File file = new File(this.configuration.getPlugin().getBootstrap().getDataDirectory(), "contexts.json"); - File oldFile = new File(this.configuration.getPlugin().getBootstrap().getDataDirectory(), "static-contexts.json"); - if (oldFile.exists()) { - oldFile.renameTo(file); + Path file = this.configuration.getPlugin().getBootstrap().getDataDirectory().resolve("contexts.json"); + Path oldFile = this.configuration.getPlugin().getBootstrap().getDataDirectory().resolve("static-contexts.json"); + if (Files.exists(oldFile)) { + try { + Files.move(oldFile, file); + } catch (IOException e) { + e.printStackTrace(); + } } - if (!file.exists()) { + if (!Files.exists(file)) { save(); return; } boolean save = false; - try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { JsonObject data = new Gson().fromJson(reader, JsonObject.class); if (data.has("context")) { @@ -91,10 +95,9 @@ public class ContextsFile { } public void save() { - File file = new File(this.configuration.getPlugin().getBootstrap().getDataDirectory(), "contexts.json"); - - try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + Path file = this.configuration.getPlugin().getBootstrap().getDataDirectory().resolve("contexts.json"); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { JsonObject data = new JsonObject(); data.add("static-contexts", ContextSetJsonSerializer.serializeContextSet(this.staticContexts)); data.add("default-contexts", ContextSetJsonSerializer.serializeContextSet(this.defaultContexts)); diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java index 2bd7f8dd..b7328872 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java @@ -34,11 +34,12 @@ import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.StorageType; -import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -57,7 +58,7 @@ public class DependencyManager { private final LuckPermsPlugin plugin; private final MessageDigest digest; private final DependencyRegistry registry; - private final EnumMap loaded = new EnumMap<>(Dependency.class); + private final EnumMap loaded = new EnumMap<>(Dependency.class); private final Map, IsolatedClassLoader> loaders = new HashMap<>(); private RelocationHandler relocationHandler = null; @@ -78,12 +79,13 @@ public class DependencyManager { return this.relocationHandler; } - private File getSaveDirectory() { - File saveDirectory = new File(this.plugin.getBootstrap().getDataDirectory(), "lib"); - if (!(saveDirectory.exists() || saveDirectory.mkdirs())) { - throw new RuntimeException("Unable to create lib dir - " + saveDirectory.getPath()); + private Path getSaveDirectory() { + Path saveDirectory = this.plugin.getBootstrap().getDataDirectory().resolve("lib"); + try { + Files.createDirectories(saveDirectory); + } catch (IOException e) { + throw new RuntimeException("Unable to create lib directory", e); } - return saveDirectory; } @@ -106,7 +108,7 @@ public class DependencyManager { .map(this.loaded::get) .map(file -> { try { - return file.toURI().toURL(); + return file.toUri().toURL(); } catch (MalformedURLException e) { throw new RuntimeException(e); } @@ -124,7 +126,7 @@ public class DependencyManager { } public void loadDependencies(Set dependencies) { - File saveDirectory = getSaveDirectory(); + Path saveDirectory = getSaveDirectory(); // create a list of file sources List sources = new ArrayList<>(); @@ -136,7 +138,7 @@ public class DependencyManager { } try { - File file = downloadDependency(saveDirectory, dependency); + Path file = downloadDependency(saveDirectory, dependency); sources.add(new Source(dependency, file)); } catch (Throwable e) { this.plugin.getLogger().severe("Exception whilst downloading dependency " + dependency.name()); @@ -156,11 +158,11 @@ public class DependencyManager { continue; } - File input = source.file; - File output = new File(input.getParentFile(), "remapped-" + input.getName()); + Path input = source.file; + Path output = input.getParent().resolve("remapped-" + input.getFileName().toString()); // if the remapped file exists already, just use that. - if (output.exists()) { + if (Files.exists(output)) { remappedJars.add(new Source(source.dependency, output)); continue; } @@ -169,7 +171,7 @@ public class DependencyManager { RelocationHandler relocationHandler = getRelocationHandler(); // attempt to remap the jar. - this.plugin.getLogger().info("Attempting to apply relocations to " + input.getName() + "..."); + this.plugin.getLogger().info("Attempting to apply relocations to " + input.getFileName().toString() + "..."); relocationHandler.remap(input, output, relocations); remappedJars.add(new Source(source.dependency, output)); @@ -190,18 +192,18 @@ public class DependencyManager { this.plugin.getBootstrap().getPluginClassLoader().loadJar(source.file); this.loaded.put(source.dependency, source.file); } catch (Throwable e) { - this.plugin.getLogger().severe("Failed to load dependency jar '" + source.file.getName() + "'."); + this.plugin.getLogger().severe("Failed to load dependency jar '" + source.file.getFileName().toString() + "'."); e.printStackTrace(); } } } - private File downloadDependency(File saveDirectory, Dependency dependency) throws Exception { + private Path downloadDependency(Path saveDirectory, Dependency dependency) throws Exception { String fileName = dependency.name().toLowerCase() + "-" + dependency.getVersion() + ".jar"; - File file = new File(saveDirectory, fileName); + Path file = saveDirectory.resolve(fileName); // if the file already exists, don't attempt to re-download it. - if (file.exists()) { + if (Files.exists(file)) { return file; } @@ -226,11 +228,11 @@ public class DependencyManager { } // if the checksum matches, save the content to disk - Files.write(file.toPath(), bytes); + Files.write(file, bytes); } // ensure the file saved correctly - if (!file.exists()) { + if (!Files.exists(file)) { throw new IllegalStateException("File not present. - " + file.toString()); } else { return file; @@ -239,9 +241,9 @@ public class DependencyManager { private static final class Source { private final Dependency dependency; - private final File file; + private final Path file; - private Source(Dependency dependency, File file) { + private Source(Dependency dependency, Path file) { this.dependency = dependency; this.file = file; } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java index d87ee9c2..ec8d45f1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java @@ -47,9 +47,12 @@ public class DependencyRegistry { )); private static final Map> STORAGE_DEPENDENCIES = ImmutableMap.>builder() - .put(StorageType.JSON, ImmutableList.of(Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON)) .put(StorageType.YAML, ImmutableList.of(Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_YAML)) + .put(StorageType.JSON, ImmutableList.of(Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON)) .put(StorageType.HOCON, ImmutableList.of(Dependency.HOCON_CONFIG, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_HOCON)) + .put(StorageType.YAML_COMBINED, ImmutableList.of(Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_YAML)) + .put(StorageType.JSON_COMBINED, ImmutableList.of(Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON)) + .put(StorageType.HOCON_COMBINED, ImmutableList.of(Dependency.HOCON_CONFIG, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_HOCON)) .put(StorageType.MONGODB, ImmutableList.of(Dependency.MONGODB_DRIVER)) .put(StorageType.MARIADB, ImmutableList.of(Dependency.MARIADB_DRIVER, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI)) .put(StorageType.MYSQL, ImmutableList.of(Dependency.MYSQL_DRIVER, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI)) diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/PluginClassLoader.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/PluginClassLoader.java index e592169b..e32de39b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/PluginClassLoader.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/PluginClassLoader.java @@ -25,8 +25,8 @@ package me.lucko.luckperms.common.dependencies.classloader; -import java.io.File; import java.net.URL; +import java.nio.file.Path; /** * Represents the plugins classloader @@ -35,6 +35,6 @@ public interface PluginClassLoader { void loadJar(URL url); - void loadJar(File file); + void loadJar(Path file); } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/ReflectionClassLoader.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/ReflectionClassLoader.java index 042c1173..49be4ced 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/ReflectionClassLoader.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/classloader/ReflectionClassLoader.java @@ -25,12 +25,12 @@ package me.lucko.luckperms.common.dependencies.classloader; -import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Path; public class ReflectionClassLoader implements PluginClassLoader { private static final Method ADD_URL_METHOD; @@ -65,9 +65,9 @@ public class ReflectionClassLoader implements PluginClassLoader { } @Override - public void loadJar(File file) { + public void loadJar(Path file) { try { - loadJar(file.toURI().toURL()); + loadJar(file.toUri().toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java index e3f328af..f4fd1e55 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java @@ -32,6 +32,7 @@ import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.nio.file.Path; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -70,14 +71,14 @@ public class RelocationHandler { } } - public void remap(File input, File output, List relocations) throws Exception { + public void remap(Path input, Path output, List relocations) throws Exception { Map mappings = new HashMap<>(); for (Relocation relocation : relocations) { mappings.put(relocation.getPattern(), relocation.getRelocatedPattern()); } // create and invoke a new relocator - Object relocator = this.jarRelocatorConstructor.newInstance(input, output, mappings); + Object relocator = this.jarRelocatorConstructor.newInstance(input.toFile(), output.toFile(), mappings); this.jarRelocatorRunMethod.invoke(relocator); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/LocaleManager.java b/common/src/main/java/me/lucko/luckperms/common/locale/LocaleManager.java index 80aeff4c..de56cde7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/LocaleManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/LocaleManager.java @@ -30,7 +30,7 @@ import me.lucko.luckperms.common.locale.command.CommandSpecData; import me.lucko.luckperms.common.locale.message.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import java.io.File; +import java.nio.file.Path; /** * Manages translations @@ -43,7 +43,7 @@ public interface LocaleManager { * @param plugin the plugin to log to * @param file the file to load from */ - void tryLoad(LuckPermsPlugin plugin, File file); + void tryLoad(LuckPermsPlugin plugin, Path file); /** * Loads a locale file @@ -51,7 +51,7 @@ public interface LocaleManager { * @param file the file to load from * @throws Exception if the process fails */ - void loadFromFile(File file) throws Exception; + void loadFromFile(Path file) throws Exception; /** * Gets the size of loaded translations diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/SimpleLocaleManager.java b/common/src/main/java/me/lucko/luckperms/common/locale/SimpleLocaleManager.java index 97a8ec6f..abe69ceb 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/SimpleLocaleManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/SimpleLocaleManager.java @@ -35,9 +35,9 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import org.yaml.snakeyaml.Yaml; import java.io.BufferedReader; -import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; public class SimpleLocaleManager implements LocaleManager { @@ -46,8 +46,8 @@ public class SimpleLocaleManager implements LocaleManager { private Map commands = ImmutableMap.of(); @Override - public void tryLoad(LuckPermsPlugin plugin, File file) { - if (file.exists()) { + public void tryLoad(LuckPermsPlugin plugin, Path file) { + if (Files.exists(file)) { plugin.getLogger().info("Found lang.yml - loading messages..."); try { loadFromFile(file); @@ -59,8 +59,8 @@ public class SimpleLocaleManager implements LocaleManager { @Override @SuppressWarnings("unchecked") - public void loadFromFile(File file) throws Exception { - try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + public void loadFromFile(Path file) throws Exception { + try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { ImmutableMap.Builder messages = ImmutableMap.builder(); ImmutableMap.Builder commands = ImmutableMap.builder(); diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index 454031b3..a931b185 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -59,7 +59,7 @@ import me.lucko.luckperms.common.tasks.UpdateTask; import me.lucko.luckperms.common.treeview.PermissionRegistry; import me.lucko.luckperms.common.verbose.VerboseHandler; -import java.io.File; +import java.io.IOException; import java.util.Optional; import java.util.Set; @@ -114,7 +114,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { // load locale this.localeManager = new SimpleLocaleManager(); - this.localeManager.tryLoad(this, new File(getBootstrap().getConfigDirectory(), "lang.yml")); + this.localeManager.tryLoad(this, getBootstrap().getConfigDirectory().resolve("lang.yml")); // now the configuration is loaded, we can create a storage factory and load initial dependencies StorageFactory storageFactory = new StorageFactory(this); @@ -127,8 +127,11 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { // initialise the storage // first, setup the file watcher, if enabled if (getConfiguration().get(ConfigKeys.WATCH_FILES)) { - this.fileWatcher = new FileWatcher(this); - getBootstrap().getScheduler().asyncRepeating(this.fileWatcher, 30L); + try { + this.fileWatcher = new FileWatcher(this, getBootstrap().getDataDirectory()); + } catch (IOException e) { + e.printStackTrace(); + } } // initialise storage diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java index 9f07d23f..bdc0d49a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java @@ -29,8 +29,8 @@ import me.lucko.luckperms.api.platform.PlatformType; import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; import me.lucko.luckperms.common.plugin.SchedulerAdapter; -import java.io.File; import java.io.InputStream; +import java.nio.file.Path; import java.util.List; import java.util.Optional; import java.util.Set; @@ -132,14 +132,14 @@ public interface LuckPermsBootstrap { * * @return the platforms data folder */ - File getDataDirectory(); + Path getDataDirectory(); /** * Gets the plugins configuration directory * * @return the config directory */ - default File getConfigDirectory() { + default Path getConfigDirectory() { return getDataDirectory(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java index 95c5d081..a1ae76aa 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java @@ -32,9 +32,11 @@ import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.dao.AbstractDao; import me.lucko.luckperms.common.storage.dao.SplitStorageDao; -import me.lucko.luckperms.common.storage.dao.file.HoconDao; -import me.lucko.luckperms.common.storage.dao.file.JsonDao; -import me.lucko.luckperms.common.storage.dao.file.YamlDao; +import me.lucko.luckperms.common.storage.dao.file.CombinedConfigurateDao; +import me.lucko.luckperms.common.storage.dao.file.SeparatedConfigurateDao; +import me.lucko.luckperms.common.storage.dao.file.loader.HoconLoader; +import me.lucko.luckperms.common.storage.dao.file.loader.JsonLoader; +import me.lucko.luckperms.common.storage.dao.file.loader.YamlLoader; import me.lucko.luckperms.common.storage.dao.mongodb.MongoDao; import me.lucko.luckperms.common.storage.dao.sql.SqlDao; import me.lucko.luckperms.common.storage.dao.sql.connection.file.H2ConnectionFactory; @@ -45,7 +47,6 @@ import me.lucko.luckperms.common.storage.dao.sql.connection.hikari.PostgreConnec import me.lucko.luckperms.common.storage.provider.StorageProviders; import me.lucko.luckperms.common.utils.ImmutableCollectors; -import java.io.File; import java.util.Map; import java.util.Set; @@ -139,13 +140,13 @@ public class StorageFactory { case SQLITE: return new SqlDao( this.plugin, - new SQLiteConnectionFactory(this.plugin, new File(this.plugin.getBootstrap().getDataDirectory(), "luckperms-sqlite.db")), + new SQLiteConnectionFactory(this.plugin, this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-sqlite.db")), this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) ); case H2: return new SqlDao( this.plugin, - new H2ConnectionFactory(this.plugin, new File(this.plugin.getBootstrap().getDataDirectory(), "luckperms-h2")), + new H2ConnectionFactory(this.plugin, this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2")), this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) ); case POSTGRESQL: @@ -162,11 +163,19 @@ public class StorageFactory { this.plugin.getConfiguration().get(ConfigKeys.MONGODB_CONNECTION_URI) ); case YAML: - return new YamlDao(this.plugin, "yaml-storage"); + return new SeparatedConfigurateDao(this.plugin, new YamlLoader(), "YAML", ".yml", "yaml-storage"); + case JSON: + return new SeparatedConfigurateDao(this.plugin, new JsonLoader(), "JSON", ".json", "json-storage"); case HOCON: - return new HoconDao(this.plugin, "hocon-storage"); + return new SeparatedConfigurateDao(this.plugin, new HoconLoader(), "HOCON", ".conf", "hocon-storage"); + case YAML_COMBINED: + return new CombinedConfigurateDao(this.plugin, new YamlLoader(), "YAML Combined", ".yml", "yaml-storage"); + case JSON_COMBINED: + return new CombinedConfigurateDao(this.plugin, new JsonLoader(), "JSON Combined", ".json", "json-storage"); + case HOCON_COMBINED: + return new CombinedConfigurateDao(this.plugin, new HoconLoader(), "HOCON Combined", ".conf", "hocon-storage"); default: - return new JsonDao(this.plugin, "json-storage"); + throw new RuntimeException("Unknown method: " + method); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java b/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java index 90245fb7..47582323 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java @@ -31,15 +31,25 @@ import java.util.List; public enum StorageType { - JSON("JSON", "json", "flatfile"), + // Config file based YAML("YAML", "yaml", "yml"), + JSON("JSON", "json", "flatfile"), HOCON("HOCON", "hocon"), + YAML_COMBINED("YAML Combined", "yaml-combined"), + JSON_COMBINED("JSON Combined", "json-combined"), + HOCON_COMBINED("HOCON Combined", "hocon-combined"), + + // Remote databases MONGODB("MongoDB", "mongodb"), MARIADB("MariaDB", "mariadb"), MYSQL("MySQL", "mysql"), POSTGRESQL("PostgreSQL", "postgresql"), + + // Local databases SQLITE("SQLite", "sqlite"), H2("H2", "h2"), + + // Custom CUSTOM("Custom", "custom"); private final String name; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/AbstractDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/AbstractDao.java index 54c5901d..31e67622 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/AbstractDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/AbstractDao.java @@ -62,7 +62,7 @@ public abstract class AbstractDao { return this.name; } - public abstract void init(); + public abstract void init() throws Exception; public abstract void shutdown(); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/SplitStorageDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/SplitStorageDao.java index 30502efc..6c949d6c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/SplitStorageDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/SplitStorageDao.java @@ -68,7 +68,7 @@ public class SplitStorageDao extends AbstractDao { } } if (failed) { - throw new RuntimeException("One of the backing failed to init"); + throw new RuntimeException("One of the backings failed to init"); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/ConfigurateDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java similarity index 62% rename from common/src/main/java/me/lucko/luckperms/common/storage/dao/file/ConfigurateDao.java rename to common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java index 9db3eae9..4b84bc37 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/ConfigurateDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java @@ -25,44 +25,40 @@ package me.lucko.luckperms.common.storage.dao.file; +import com.google.common.base.Throwables; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import me.lucko.luckperms.api.ChatMetaType; -import me.lucko.luckperms.api.HeldPermission; import me.lucko.luckperms.api.LogEntry; import me.lucko.luckperms.api.Node; import me.lucko.luckperms.api.context.ImmutableContextSet; import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.contexts.ContextSetConfigurateSerializer; -import me.lucko.luckperms.common.managers.group.GroupManager; -import me.lucko.luckperms.common.managers.track.TrackManager; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.MetaType; import me.lucko.luckperms.common.node.NodeFactory; -import me.lucko.luckperms.common.node.NodeHeldPermission; import me.lucko.luckperms.common.node.NodeModel; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.references.UserIdentifier; import me.lucko.luckperms.common.storage.PlayerSaveResult; import me.lucko.luckperms.common.storage.dao.AbstractDao; +import me.lucko.luckperms.common.storage.dao.file.loader.ConfigurateLoader; +import me.lucko.luckperms.common.storage.dao.file.loader.JsonLoader; import me.lucko.luckperms.common.utils.ImmutableCollectors; -import me.lucko.luckperms.common.utils.Uuids; +import me.lucko.luckperms.common.utils.MoreFiles; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.SimpleConfigurationNode; import ninja.leaping.configurate.Types; -import ninja.leaping.configurate.loader.ConfigurationLoader; -import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -76,143 +72,70 @@ import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; -public abstract class ConfigurateDao extends AbstractDao { +/** + * Abstract implementation using configurate {@link ConfigurationNode}s to serialize and deserialize + * data. + */ +public abstract class AbstractConfigurateDao extends AbstractDao { + + // the loader responsible for i/o + protected final ConfigurateLoader loader; + + // the name of the data directory + private final String dataDirectoryName; + // the data directory + protected Path dataDirectory; + + // the uuid cache instance private final FileUuidCache uuidCache = new FileUuidCache(); + // the action logger instance private final FileActionLogger actionLogger = new FileActionLogger(); - private final String fileExtension; - private final String dataFolderName; + // the file used to store uuid data + private Path uuidDataFile; + // the file used to store logged actions + private Path actionLogFile; - private File uuidDataFile; - private File actionLogFile; - - private File usersDirectory; - private File groupsDirectory; - private File tracksDirectory; - - protected ConfigurateDao(LuckPermsPlugin plugin, String name, String fileExtension, String dataFolderName) { + protected AbstractConfigurateDao(LuckPermsPlugin plugin, ConfigurateLoader loader, String name, String dataDirectoryName) { super(plugin, name); - this.fileExtension = fileExtension; - this.dataFolderName = dataFolderName; + this.loader = loader; + this.dataDirectoryName = dataDirectoryName; } - public String getFileExtension() { - return this.fileExtension; - } + /** + * Reads a configuration node from the given location + * + * @param location the location + * @param name the name of the object + * @return the node + * @throws IOException if an io error occurs + */ + protected abstract ConfigurationNode readFile(StorageLocation location, String name) throws IOException; - protected abstract ConfigurationLoader loader(Path path); + /** + * Saves a configuration node to the given location + * + * @param location the location + * @param name the name of the object + * @param node the node + * @throws IOException if an io error occurs + */ + protected abstract void saveFile(StorageLocation location, String name, ConfigurationNode node) throws IOException; - private ConfigurationNode readFile(StorageLocation location, String name) throws IOException { - File file = new File(getDirectory(location), name + this.fileExtension); - registerFileAction(location, file); - return readFile(file); - } - - private ConfigurationNode readFile(File file) throws IOException { - if (!file.exists()) { - return null; - } - - return loader(file.toPath()).load(); - } - - private void saveFile(StorageLocation location, String name, ConfigurationNode node) throws IOException { - File file = new File(getDirectory(location), name + this.fileExtension); - registerFileAction(location, file); - saveFile(file, node); - } - - private void saveFile(File file, ConfigurationNode node) throws IOException { - if (node == null) { - if (file.exists()) { - file.delete(); - } - return; - } - - loader(file.toPath()).save(node); - } - - private File getDirectory(StorageLocation location) { - switch (location) { - case USER: - return this.usersDirectory; - case GROUP: - return this.groupsDirectory; - case TRACK: - return this.tracksDirectory; - default: - throw new RuntimeException(); - } - } - - private FilenameFilter getFileTypeFilter() { - return (dir, name) -> name.endsWith(this.fileExtension); - } - - private Exception reportException(String file, Exception ex) throws Exception { + // used to report i/o exceptions which took place in a specific file + protected RuntimeException reportException(String file, Exception ex) throws RuntimeException { this.plugin.getLogger().warn("Exception thrown whilst performing i/o: " + file); ex.printStackTrace(); - throw ex; - } - - private void registerFileAction(StorageLocation type, File file) { - this.plugin.getFileWatcher().ifPresent(fileWatcher -> fileWatcher.registerChange(type, file.getName())); + throw Throwables.propagate(ex); } @Override - public void init() { - try { - File data = FileUtils.mkdirs(new File(this.plugin.getBootstrap().getDataDirectory(), this.dataFolderName)); + public void init() throws IOException { + this.dataDirectory = this.plugin.getBootstrap().getDataDirectory().resolve(this.dataDirectoryName); + Files.createDirectories(this.dataDirectory); - this.usersDirectory = FileUtils.mkdir(new File(data, "users")); - this.groupsDirectory = FileUtils.mkdir(new File(data, "groups")); - this.tracksDirectory = FileUtils.mkdir(new File(data, "tracks")); - this.uuidDataFile = FileUtils.createNewFile(new File(data, "uuidcache.txt")); - this.actionLogFile = FileUtils.createNewFile(new File(data, "actions.log")); - - // Listen for file changes. - this.plugin.getFileWatcher().ifPresent(watcher -> { - watcher.subscribe("user", this.usersDirectory.toPath(), s -> { - if (!s.endsWith(this.fileExtension)) { - return; - } - - String user = s.substring(0, s.length() - this.fileExtension.length()); - UUID uuid = Uuids.parseNullable(user); - if (uuid == null) { - return; - } - - User u = this.plugin.getUserManager().getIfLoaded(uuid); - if (u != null) { - this.plugin.getLogger().info("[FileWatcher] Refreshing user " + u.getFriendlyName()); - this.plugin.getStorage().loadUser(uuid, null); - } - }); - watcher.subscribe("group", this.groupsDirectory.toPath(), s -> { - if (!s.endsWith(this.fileExtension)) { - return; - } - - String groupName = s.substring(0, s.length() - this.fileExtension.length()); - this.plugin.getLogger().info("[FileWatcher] Refreshing group " + groupName); - this.plugin.getUpdateTaskBuffer().request(); - }); - watcher.subscribe("track", this.tracksDirectory.toPath(), s -> { - if (!s.endsWith(this.fileExtension)) { - return; - } - - String trackName = s.substring(0, s.length() - this.fileExtension.length()); - this.plugin.getLogger().info("[FileWatcher] Refreshing track " + trackName); - this.plugin.getStorage().loadAllTracks(); - }); - }); - } catch (IOException e) { - e.printStackTrace(); - return; - } + this.uuidDataFile = MoreFiles.createFileIfNotExists(this.dataDirectory.resolve("uuidcache.txt")); + this.actionLogFile = MoreFiles.createFileIfNotExists(this.dataDirectory.resolve("actions.log")); this.uuidCache.load(this.uuidDataFile); this.actionLogger.init(this.actionLogFile); @@ -235,70 +158,30 @@ public abstract class ConfigurateDao extends AbstractDao { return Log.empty(); } - @Override - public void applyBulkUpdate(BulkUpdate bulkUpdate) throws Exception { - if (bulkUpdate.getDataType().isIncludingUsers()) { - File[] files = getDirectory(StorageLocation.USER).listFiles(getFileTypeFilter()); - if (files == null) { - throw new IllegalStateException("Users directory matched no files."); - } + protected ConfigurationNode processBulkUpdate(BulkUpdate bulkUpdate, ConfigurationNode node) { + Set nodes = readNodes(node); + Set results = nodes.stream() + .map(bulkUpdate::apply) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); - for (File file : files) { - try { - registerFileAction(StorageLocation.USER, file); - ConfigurationNode object = readFile(file); - Set nodes = readNodes(object); - Set results = nodes.stream() - .map(bulkUpdate::apply) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - if (!nodes.equals(results)) { - writeNodes(object, results); - saveFile(file, object); - } - } catch (Exception e) { - throw reportException(file.getName(), e); - } - } + if (nodes.equals(results)) { + return null; } - if (bulkUpdate.getDataType().isIncludingGroups()) { - File[] files = getDirectory(StorageLocation.GROUP).listFiles(getFileTypeFilter()); - if (files == null) { - throw new IllegalStateException("Groups directory matched no files."); - } - - for (File file : files) { - try { - registerFileAction(StorageLocation.GROUP, file); - ConfigurationNode object = readFile(file); - Set nodes = readNodes(object); - Set results = nodes.stream() - .map(bulkUpdate::apply) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - if (!nodes.equals(results)) { - writeNodes(object, results); - saveFile(file, object); - } - } catch (Exception e) { - throw reportException(file.getName(), e); - } - } - } + writeNodes(node, results); + return node; } @Override - public User loadUser(UUID uuid, String username) throws Exception { + public User loadUser(UUID uuid, String username) { User user = this.plugin.getUserManager().getOrMake(UserIdentifier.of(uuid, username)); user.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.USER, uuid.toString()); if (object != null) { String name = object.getNode("name").getString(); - user.getPrimaryGroup().setStoredValue(object.getNode(this instanceof JsonDao ? "primaryGroup" : "primary-group").getString()); + user.getPrimaryGroup().setStoredValue(object.getNode(this.loader instanceof JsonLoader ? "primaryGroup" : "primary-group").getString()); Set nodes = readNodes(object).stream().map(NodeModel::toNode).collect(Collectors.toSet()); user.setEnduringNodes(nodes); @@ -329,16 +212,18 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public void saveUser(User user) throws Exception { + public void saveUser(User user) { user.getIoLock().lock(); try { if (!this.plugin.getUserManager().shouldSave(user)) { saveFile(StorageLocation.USER, user.getUuid().toString(), null); } else { ConfigurationNode data = SimpleConfigurationNode.root(); - data.getNode("uuid").setValue(user.getUuid().toString()); + if (this instanceof SeparatedConfigurateDao) { + data.getNode("uuid").setValue(user.getUuid().toString()); + } data.getNode("name").setValue(user.getName().orElse("null")); - data.getNode(this instanceof JsonDao ? "primaryGroup" : "primary-group").setValue(user.getPrimaryGroup().getStoredValue().orElse(NodeFactory.DEFAULT_GROUP_NAME)); + data.getNode(this.loader instanceof JsonLoader ? "primaryGroup" : "primary-group").setValue(user.getPrimaryGroup().getStoredValue().orElse(NodeFactory.DEFAULT_GROUP_NAME)); Set nodes = user.getEnduringNodes().values().stream().map(NodeModel::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); writeNodes(data, nodes); @@ -353,44 +238,7 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public Set getUniqueUsers() { - String[] fileNames = this.usersDirectory.list(getFileTypeFilter()); - if (fileNames == null) return null; - return Arrays.stream(fileNames) - .map(s -> s.substring(0, s.length() - this.fileExtension.length())) - .map(UUID::fromString) - .collect(Collectors.toSet()); - } - - @Override - public List> getUsersWithPermission(String permission) throws Exception { - List> held = new ArrayList<>(); - File[] files = getDirectory(StorageLocation.USER).listFiles(getFileTypeFilter()); - if (files == null) { - throw new IllegalStateException("Users directory matched no files."); - } - - for (File file : files) { - try { - registerFileAction(StorageLocation.USER, file); - ConfigurationNode object = readFile(file); - UUID holder = UUID.fromString(file.getName().substring(0, file.getName().length() - this.fileExtension.length())); - Set nodes = readNodes(object); - for (NodeModel e : nodes) { - if (!e.getPermission().equalsIgnoreCase(permission)) { - continue; - } - held.add(NodeHeldPermission.of(holder, e)); - } - } catch (Exception e) { - throw reportException(file.getName(), e); - } - } - return held; - } - - @Override - public Group createAndLoadGroup(String name) throws Exception { + public Group createAndLoadGroup(String name) { Group group = this.plugin.getGroupManager().getOrMake(name); group.getIoLock().lock(); try { @@ -401,7 +249,9 @@ public abstract class ConfigurateDao extends AbstractDao { group.setEnduringNodes(nodes); } else { ConfigurationNode data = SimpleConfigurationNode.root(); - data.getNode("name").setValue(group.getName()); + if (this instanceof SeparatedConfigurateDao) { + data.getNode("name").setValue(group.getName()); + } Set nodes = group.getEnduringNodes().values().stream().map(NodeModel::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); writeNodes(data, nodes); @@ -418,7 +268,7 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public Optional loadGroup(String name) throws Exception { + public Optional loadGroup(String name) { Group group = this.plugin.getGroupManager().getIfLoaded(name); if (group != null) { group.getIoLock().lock(); @@ -452,41 +302,13 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public void loadAllGroups() throws IOException { - String[] fileNames = this.groupsDirectory.list(getFileTypeFilter()); - if (fileNames == null) { - throw new IOException("Not a directory"); - } - List groups = Arrays.stream(fileNames) - .map(s -> s.substring(0, s.length() - this.fileExtension.length())) - .collect(Collectors.toList()); - - boolean success = true; - for (String g : groups) { - try { - loadGroup(g); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { - throw new RuntimeException("Exception occurred whilst loading a group"); - } - - GroupManager gm = this.plugin.getGroupManager(); - gm.getAll().values().stream() - .filter(g -> !groups.contains(g.getName())) - .forEach(gm::unload); - } - - @Override - public void saveGroup(Group group) throws Exception { + public void saveGroup(Group group) { group.getIoLock().lock(); try { ConfigurationNode data = SimpleConfigurationNode.root(); - data.getNode("name").setValue(group.getName()); + if (this instanceof SeparatedConfigurateDao) { + data.getNode("name").setValue(group.getName()); + } Set nodes = group.getEnduringNodes().values().stream().map(NodeModel::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); writeNodes(data, nodes); @@ -500,15 +322,10 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public void deleteGroup(Group group) throws Exception { + public void deleteGroup(Group group) { group.getIoLock().lock(); try { - File groupFile = new File(this.groupsDirectory, group.getName() + this.fileExtension); - registerFileAction(StorageLocation.GROUP, groupFile); - - if (groupFile.exists()) { - groupFile.delete(); - } + saveFile(StorageLocation.GROUP, group.getName(), null); } catch (Exception e) { throw reportException(group.getName(), e); } finally { @@ -518,34 +335,7 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public List> getGroupsWithPermission(String permission) throws Exception { - List> held = new ArrayList<>(); - File[] files = getDirectory(StorageLocation.GROUP).listFiles(getFileTypeFilter()); - if (files == null) { - throw new IllegalStateException("Groups directory matched no files."); - } - - for (File file : files) { - try { - registerFileAction(StorageLocation.GROUP, file); - ConfigurationNode object = readFile(file); - String holder = file.getName().substring(0, file.getName().length() - this.fileExtension.length()); - Set nodes = readNodes(object); - for (NodeModel e : nodes) { - if (!e.getPermission().equalsIgnoreCase(permission)) { - continue; - } - held.add(NodeHeldPermission.of(holder, e)); - } - } catch (Exception e) { - throw reportException(file.getName(), e); - } - } - return held; - } - - @Override - public Track createAndLoadTrack(String name) throws Exception { + public Track createAndLoadTrack(String name) { Track track = this.plugin.getTrackManager().getOrMake(name); track.getIoLock().lock(); try { @@ -559,7 +349,9 @@ public abstract class ConfigurateDao extends AbstractDao { track.setGroups(groups); } else { ConfigurationNode data = SimpleConfigurationNode.root(); - data.getNode("name").setValue(name); + if (this instanceof SeparatedConfigurateDao) { + data.getNode("name").setValue(name); + } data.getNode("groups").setValue(track.getGroups()); saveFile(StorageLocation.TRACK, name, data); } @@ -573,7 +365,7 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public Optional loadTrack(String name) throws Exception { + public Optional loadTrack(String name) { Track track = this.plugin.getTrackManager().getIfLoaded(name); if (track != null) { track.getIoLock().lock(); @@ -608,41 +400,13 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public void loadAllTracks() throws IOException { - String[] fileNames = this.tracksDirectory.list(getFileTypeFilter()); - if (fileNames == null) { - throw new IOException("Not a directory"); - } - List tracks = Arrays.stream(fileNames) - .map(s -> s.substring(0, s.length() - this.fileExtension.length())) - .collect(Collectors.toList()); - - boolean success = true; - for (String t : tracks) { - try { - loadTrack(t); - } catch (Exception e) { - e.printStackTrace(); - success = false; - } - } - - if (!success) { - throw new RuntimeException("Exception occurred whilst loading a track"); - } - - TrackManager tm = this.plugin.getTrackManager(); - tm.getAll().values().stream() - .filter(t -> !tracks.contains(t.getName())) - .forEach(tm::unload); - } - - @Override - public void saveTrack(Track track) throws Exception { + public void saveTrack(Track track) { track.getIoLock().lock(); try { ConfigurationNode data = SimpleConfigurationNode.root(); - data.getNode("name").setValue(track.getName()); + if (this instanceof SeparatedConfigurateDao) { + data.getNode("name").setValue(track.getName()); + } data.getNode("groups").setValue(track.getGroups()); saveFile(StorageLocation.TRACK, track.getName(), data); } catch (Exception e) { @@ -653,15 +417,10 @@ public abstract class ConfigurateDao extends AbstractDao { } @Override - public void deleteTrack(Track track) throws Exception { + public void deleteTrack(Track track) { track.getIoLock().lock(); try { - File trackFile = new File(this.tracksDirectory, track.getName() + this.fileExtension); - registerFileAction(StorageLocation.TRACK, trackFile); - - if (trackFile.exists()) { - trackFile.delete(); - } + saveFile(StorageLocation.TRACK, track.getName(), null); } catch (Exception e) { throw reportException(track.getName(), e); } finally { @@ -736,7 +495,7 @@ public abstract class ConfigurateDao extends AbstractDao { return Maps.immutableEntry(entry.getKey().toString(), entry.getValue()); } - private static Set readNodes(ConfigurationNode data) { + protected static Set readNodes(ConfigurationNode data) { Set nodes = new HashSet<>(); if (data.getNode("permissions").hasListChildren()) { @@ -924,5 +683,4 @@ public abstract class ConfigurateDao extends AbstractDao { to.getNode("meta").setValue(metaSection); } } - } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/CombinedConfigurateDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/CombinedConfigurateDao.java new file mode 100644 index 00000000..359943d8 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/CombinedConfigurateDao.java @@ -0,0 +1,372 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.storage.dao.file; + +import me.lucko.luckperms.api.HeldPermission; +import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.managers.group.GroupManager; +import me.lucko.luckperms.common.managers.track.TrackManager; +import me.lucko.luckperms.common.node.NodeHeldPermission; +import me.lucko.luckperms.common.node.NodeModel; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.dao.file.loader.ConfigurateLoader; + +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.loader.ConfigurationLoader; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class CombinedConfigurateDao extends AbstractConfigurateDao { + private final String fileExtension; + + private Path usersFile; + private Path groupsFile; + private Path tracksFile; + + private CachedLoader usersLoader; + private CachedLoader groupsLoader; + private CachedLoader tracksLoader; + + private final class CachedLoader { + private final Path path; + + private final ConfigurationLoader loader; + private ConfigurationNode node = null; + private final ReentrantLock lock = new ReentrantLock(); + + private CachedLoader(Path path) { + this.path = path; + this.loader = CombinedConfigurateDao.super.loader.loader(path); + reload(); + } + + private void recordChange() { + if (CombinedConfigurateDao.this.watcher != null) { + CombinedConfigurateDao.this.watcher.recordChange(this.path.getFileName().toString()); + } + } + + public ConfigurationNode getNode() throws IOException { + this.lock.lock(); + try { + if (this.node == null) { + this.node = this.loader.load(); + } + + return this.node; + } finally { + this.lock.unlock(); + } + } + + public void apply(Consumer action) throws IOException { + apply(false, false, action); + } + + public void apply(boolean save, boolean reload, Consumer action) throws IOException { + this.lock.lock(); + try { + if (this.node == null || reload) { + recordChange(); + this.node = this.loader.load(); + } + + action.accept(this.node); + + if (save) { + recordChange(); + this.loader.save(this.node); + } + } finally { + this.lock.unlock(); + } + } + + public void save() throws IOException { + this.lock.lock(); + try { + recordChange(); + this.loader.save(this.node); + } finally { + this.lock.unlock(); + } + } + + public void reload() { + this.lock.lock(); + try { + this.node = null; + try { + recordChange(); + this.node = this.loader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + } finally { + this.lock.unlock(); + } + } + } + + private FileWatcher.WatchedLocation watcher = null; + + /** + * Creates a new configurate dao + * + * @param plugin the plugin instance + * @param name the name of this dao + * @param fileExtension the file extension used by this instance, including a "." at the start + * @param dataFolderName the name of the folder used to store data + */ + public CombinedConfigurateDao(LuckPermsPlugin plugin, ConfigurateLoader loader, String name, String fileExtension, String dataFolderName) { + super(plugin, loader, name, dataFolderName); + this.fileExtension = fileExtension; + } + + @Override + protected ConfigurationNode readFile(StorageLocation location, String name) throws IOException { + ConfigurationNode root = getStorageLoader(location).getNode(); + ConfigurationNode ret = root.getNode(name); + return ret.isVirtual() ? null : ret; + } + + @Override + protected void saveFile(StorageLocation location, String name, ConfigurationNode node) throws IOException { + getStorageLoader(location).apply(true, false, root -> root.getNode(name).setValue(node)); + } + + private CachedLoader getStorageLoader(StorageLocation location) { + switch (location) { + case USER: + return this.usersLoader; + case GROUP: + return this.groupsLoader; + case TRACK: + return this.tracksLoader; + default: + throw new RuntimeException(); + } + } + + @Override + public void init() throws IOException { + super.init(); + + this.usersFile = super.dataDirectory.resolve("users" + this.fileExtension); + this.groupsFile = super.dataDirectory.resolve("groups" + this.fileExtension); + this.tracksFile = super.dataDirectory.resolve("tracks" + this.fileExtension); + + this.usersLoader = new CachedLoader(this.usersFile); + this.groupsLoader = new CachedLoader(this.groupsFile); + this.tracksLoader = new CachedLoader(this.tracksFile); + + // Listen for file changes. + FileWatcher watcher = this.plugin.getFileWatcher().orElse(null); + if (watcher != null) { + this.watcher = watcher.getWatcher(super.dataDirectory); + this.watcher.addListener(path -> { + if (path.getFileName().equals(this.usersFile.getFileName())) { + this.plugin.getLogger().info("[FileWatcher] Detected change in users file - reloading..."); + this.usersLoader.reload(); + this.plugin.getUpdateTaskBuffer().request(); + } else if (path.getFileName().equals(this.groupsFile.getFileName())) { + this.plugin.getLogger().info("[FileWatcher] Detected change in groups file - reloading..."); + this.groupsLoader.reload(); + this.plugin.getUpdateTaskBuffer().request(); + } else if (path.getFileName().equals(this.tracksFile.getFileName())) { + this.plugin.getLogger().info("[FileWatcher] Detected change in tracks file - reloading..."); + this.tracksLoader.reload(); + this.plugin.getStorage().loadAllTracks(); + } + }); + } + } + + @Override + public void shutdown() { + try { + this.usersLoader.save(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + this.groupsLoader.save(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + this.tracksLoader.save(); + } catch (IOException e) { + e.printStackTrace(); + } + super.shutdown(); + } + + @Override + public void applyBulkUpdate(BulkUpdate bulkUpdate) throws Exception { + if (bulkUpdate.getDataType().isIncludingUsers()) { + this.usersLoader.apply(true, true, root -> { + for (Map.Entry entry : root.getChildrenMap().entrySet()) { + processBulkUpdate(bulkUpdate, entry.getValue()); + } + }); + } + + if (bulkUpdate.getDataType().isIncludingGroups()) { + this.groupsLoader.apply(true, true, root -> { + for (Map.Entry entry : root.getChildrenMap().entrySet()) { + processBulkUpdate(bulkUpdate, entry.getValue()); + } + }); + } + } + + @Override + public Set getUniqueUsers() throws IOException { + return this.usersLoader.getNode().getChildrenMap().keySet().stream() + .map(Object::toString) + .map(UUID::fromString) + .collect(Collectors.toSet()); + } + + @Override + public List> getUsersWithPermission(String permission) throws Exception { + List> held = new ArrayList<>(); + this.usersLoader.apply(false, true, root -> { + for (Map.Entry entry : root.getChildrenMap().entrySet()) { + try { + UUID holder = UUID.fromString(entry.getKey().toString()); + ConfigurationNode object = entry.getValue(); + + Set nodes = readNodes(object); + for (NodeModel e : nodes) { + if (!e.getPermission().equalsIgnoreCase(permission)) { + continue; + } + held.add(NodeHeldPermission.of(holder, e)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + return held; + } + + @Override + public void loadAllGroups() throws IOException { + List groups = new ArrayList<>(); + + this.groupsLoader.apply(false, true, root -> { + groups.addAll(root.getChildrenMap().keySet().stream() + .map(Object::toString) + .collect(Collectors.toList())); + }); + + boolean success = true; + for (String g : groups) { + try { + loadGroup(g); + } catch (Exception e) { + e.printStackTrace(); + success = false; + } + } + + if (!success) { + throw new RuntimeException("Exception occurred whilst loading a group"); + } + + GroupManager gm = this.plugin.getGroupManager(); + gm.getAll().values().stream() + .filter(g -> !groups.contains(g.getName())) + .forEach(gm::unload); + } + + @Override + public List> getGroupsWithPermission(String permission) throws Exception { + List> held = new ArrayList<>(); + this.groupsLoader.apply(false, true, root -> { + for (Map.Entry entry : root.getChildrenMap().entrySet()) { + try { + String holder = entry.getKey().toString(); + ConfigurationNode object = entry.getValue(); + + Set nodes = readNodes(object); + for (NodeModel e : nodes) { + if (!e.getPermission().equalsIgnoreCase(permission)) { + continue; + } + held.add(NodeHeldPermission.of(holder, e)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + return held; + } + + @Override + public void loadAllTracks() throws IOException { + List tracks = new ArrayList<>(); + + this.tracksLoader.apply(false, true, root -> { + tracks.addAll(root.getChildrenMap().keySet().stream() + .map(Object::toString) + .collect(Collectors.toList())); + }); + + boolean success = true; + for (String t : tracks) { + try { + loadTrack(t); + } catch (Exception e) { + e.printStackTrace(); + success = false; + } + } + + if (!success) { + throw new RuntimeException("Exception occurred whilst loading a track"); + } + + TrackManager tm = this.plugin.getTrackManager(); + tm.getAll().values().stream() + .filter(t -> !tracks.contains(t.getName())) + .forEach(tm::unload); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java index a8e90863..7c878057 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java @@ -28,7 +28,7 @@ package me.lucko.luckperms.common.storage.dao.file; import me.lucko.luckperms.api.LogEntry; import me.lucko.luckperms.common.command.CommandManager; -import java.io.File; +import java.nio.file.Path; import java.util.Date; import java.util.logging.FileHandler; import java.util.logging.Formatter; @@ -40,9 +40,9 @@ public class FileActionLogger { private static final String LOG_FORMAT = "%s(%s): [%s] %s(%s) --> %s"; private final Logger actionLogger = Logger.getLogger("luckperms_actions"); - public void init(File file) { + public void init(Path file) { try { - FileHandler fh = new FileHandler(file.getAbsolutePath(), 0, 1, true); + FileHandler fh = new FileHandler(file.toString(), 0, 1, true); fh.setFormatter(new Formatter() { @Override public String format(LogRecord record) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUuidCache.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUuidCache.java index 064e20d8..af44cad4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUuidCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUuidCache.java @@ -35,10 +35,10 @@ import me.lucko.luckperms.common.utils.Uuids; import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -182,12 +182,12 @@ public class FileUuidCache { } } - public void load(File file) { - if (!file.exists()) { + public void load(Path file) { + if (!Files.exists(file)) { return; } - try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { String entry; while ((entry = reader.readLine()) != null) { entry = entry.trim(); @@ -201,8 +201,8 @@ public class FileUuidCache { } } - public void save(File file) { - try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + public void save(Path file) { + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { writer.write("# LuckPerms UUID lookup cache"); writer.newLine(); for (Map.Entry ent : this.lookupMap.entrySet()) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileWatcher.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileWatcher.java index e68b2525..fa0c5151 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileWatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileWatcher.java @@ -26,6 +26,7 @@ package me.lucko.luckperms.common.storage.dao.file; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.utils.Iterators; import java.io.IOException; import java.nio.file.Path; @@ -38,54 +39,35 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -public class FileWatcher implements Runnable { +public class FileWatcher { private static final WatchEvent.Kind[] KINDS = new WatchEvent.Kind[]{ StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY }; - private final LuckPermsPlugin plugin; + private final Path basePath; + private final Map watchedLocations; - private final Map keyMap; - private final Map internalChanges; - private WatchService watchService = null; + // the watchservice instance + private final WatchService watchService; - public FileWatcher(LuckPermsPlugin plugin) { - this.plugin = plugin; - this.keyMap = Collections.synchronizedMap(new HashMap<>()); - this.internalChanges = Collections.synchronizedMap(new HashMap<>()); - try { - this.watchService = plugin.getBootstrap().getDataDirectory().toPath().getFileSystem().newWatchService(); - } catch (IOException e) { - e.printStackTrace(); - } + public FileWatcher(LuckPermsPlugin plugin, Path basePath) throws IOException { + this.watchedLocations = Collections.synchronizedMap(new HashMap<>()); + this.basePath = basePath; + this.watchService = basePath.getFileSystem().newWatchService(); + + plugin.getBootstrap().getScheduler().asyncLater(this::initLocations, 25L); + plugin.getBootstrap().getScheduler().asyncRepeating(this::tick, 10L); } - public void subscribe(String id, Path path, Consumer consumer) { - if (this.watchService == null) { - return; - } - - // Register with a delay to ignore changes made at startup - this.plugin.getBootstrap().getScheduler().asyncLater(() -> { - this.keyMap.computeIfAbsent(id, s -> { - WatchKey key; - try { - key = path.register(this.watchService, KINDS); - } catch (IOException e) { - throw new RuntimeException(e); - } - return new WatchedLocation(path, key, consumer); - }); - }, 40L); - } - - public void registerChange(StorageLocation location, String fileName) { - this.internalChanges.put(location.name().toLowerCase() + "/" + fileName, System.currentTimeMillis()); + public WatchedLocation getWatcher(Path path) { + Path relativePath = this.basePath.relativize(path); + return this.watchedLocations.computeIfAbsent(relativePath, p -> new WatchedLocation(this, p)); } public void close() { @@ -100,21 +82,78 @@ public class FileWatcher implements Runnable { } } - @Override - public void run() { - long expireTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(4); - // was either processed last time, or recently modified by the system. - this.internalChanges.values().removeIf(lastChange -> lastChange < expireTime); + private void initLocations() { + for (WatchedLocation loc : this.watchedLocations.values()) { + try { + loc.setup(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } - List expired = new ArrayList<>(); + private void tick() { + List expired = new ArrayList<>(); + for (Map.Entry ent : this.watchedLocations.entrySet()) { + boolean valid = ent.getValue().tick(); + if (!valid) { + new RuntimeException("WatchKey no longer valid: " + ent.getKey().toString()).printStackTrace(); + expired.add(ent.getKey()); + } + } + expired.forEach(this.watchedLocations::remove); + } - for (Map.Entry ent : this.keyMap.entrySet()) { - String id = ent.getKey(); - Path path = ent.getValue().getPath(); - WatchKey key = ent.getValue().getKey(); + /** + * Encapsulates a "watcher" in a specific directory. + */ + public static final class WatchedLocation { + // the parent watcher + private final FileWatcher watcher; - List> watchEvents = key.pollEvents(); + // the relative path to the directory being watched + private final Path relativePath; + // the absolute path to the directory being watched + private final Path absolutePath; + + // the times of recent changes + private final Map lastChange = Collections.synchronizedMap(new HashMap<>()); + + // if the key is registered + private boolean ready = false; + + // the watch key + private WatchKey key = null; + + // the callback functions + private final List> callbacks = new CopyOnWriteArrayList<>(); + + private WatchedLocation(FileWatcher watcher, Path relativePath) { + this.watcher = watcher; + this.relativePath = relativePath; + this.absolutePath = this.watcher.basePath.resolve(this.relativePath); + } + + private synchronized void setup() throws IOException { + if (this.ready) { + return; + } + + this.key = this.absolutePath.register(this.watcher.watchService, KINDS); + this.ready = true; + } + + private boolean tick() { + if (!this.ready) { + return true; + } + + // remove old change entries. + long expireTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(4); + this.lastChange.values().removeIf(lastChange -> lastChange < expireTime); + + List> watchEvents = this.key.pollEvents(); for (WatchEvent event : watchEvents) { Path context = (Path) event.context(); @@ -122,7 +161,6 @@ public class FileWatcher implements Runnable { continue; } - Path file = path.resolve(context); String fileName = context.toString(); // ignore temporary changes @@ -130,50 +168,26 @@ public class FileWatcher implements Runnable { continue; } - if (this.internalChanges.containsKey(id + "/" + fileName)) { - // This file was modified by the system. + // ignore changes already registered to the system + if (this.lastChange.containsKey(fileName)) { continue; } + this.lastChange.put(fileName, System.currentTimeMillis()); - this.internalChanges.put(id + "/" + fileName, System.currentTimeMillis()); - - this.plugin.getLogger().info("[FileWatcher] Detected change in file: " + file.toString()); - - // Process the change - ent.getValue().getFileConsumer().accept(fileName); + // process the change + Iterators.iterate(this.callbacks, cb -> cb.accept(context)); } - boolean valid = key.reset(); - if (!valid) { - new RuntimeException("WatchKey no longer valid: " + key.toString()).printStackTrace(); - expired.add(id); - } + // reset the watch key. + return this.key.reset(); } - expired.forEach(this.keyMap::remove); - } - - private static class WatchedLocation { - private final Path path; - private final WatchKey key; - private final Consumer fileConsumer; - - public WatchedLocation(Path path, WatchKey key, Consumer fileConsumer) { - this.path = path; - this.key = key; - this.fileConsumer = fileConsumer; + public void recordChange(String fileName) { + this.lastChange.put(fileName, System.currentTimeMillis()); } - public Path getPath() { - return this.path; - } - - public WatchKey getKey() { - return this.key; - } - - public Consumer getFileConsumer() { - return this.fileConsumer; + public void addListener(Consumer updateConsumer) { + this.callbacks.add(updateConsumer); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/SeparatedConfigurateDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/SeparatedConfigurateDao.java new file mode 100644 index 00000000..53c49e1d --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/SeparatedConfigurateDao.java @@ -0,0 +1,366 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.storage.dao.file; + +import me.lucko.luckperms.api.HeldPermission; +import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.managers.group.GroupManager; +import me.lucko.luckperms.common.managers.track.TrackManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.node.NodeHeldPermission; +import me.lucko.luckperms.common.node.NodeModel; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.dao.file.loader.ConfigurateLoader; +import me.lucko.luckperms.common.utils.Uuids; + +import ninja.leaping.configurate.ConfigurationNode; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SeparatedConfigurateDao extends AbstractConfigurateDao { + private final String fileExtension; + + private Path usersDirectory; + private Path groupsDirectory; + private Path tracksDirectory; + + private FileWatcher.WatchedLocation userWatcher = null; + private FileWatcher.WatchedLocation groupWatcher = null; + private FileWatcher.WatchedLocation trackWatcher = null; + + /** + * Creates a new configurate dao + * + * @param plugin the plugin instance + * @param name the name of this dao + * @param fileExtension the file extension used by this instance, including a "." at the start + * @param dataFolderName the name of the folder used to store data + */ + public SeparatedConfigurateDao(LuckPermsPlugin plugin, ConfigurateLoader loader, String name, String fileExtension, String dataFolderName) { + super(plugin, loader, name, dataFolderName); + this.fileExtension = fileExtension; + } + + @Override + protected ConfigurationNode readFile(StorageLocation location, String name) throws IOException { + Path file = getDirectory(location).resolve(name + this.fileExtension); + registerFileAction(location, file); + return readFile(file); + } + + private ConfigurationNode readFile(Path file) throws IOException { + if (!Files.exists(file)) { + return null; + } + + return this.loader.loader(file).load(); + } + + @Override + protected void saveFile(StorageLocation location, String name, ConfigurationNode node) throws IOException { + Path file = getDirectory(location).resolve(name + this.fileExtension); + registerFileAction(location, file); + saveFile(file, node); + } + + private void saveFile(Path file, ConfigurationNode node) throws IOException { + if (node == null) { + Files.deleteIfExists(file); + return; + } + + this.loader.loader(file).save(node); + } + + private Path getDirectory(StorageLocation location) { + switch (location) { + case USER: + return this.usersDirectory; + case GROUP: + return this.groupsDirectory; + case TRACK: + return this.tracksDirectory; + default: + throw new RuntimeException(); + } + } + + private Predicate getFileTypeFilter() { + return path -> path.getFileName().toString().endsWith(this.fileExtension); + } + + private void registerFileAction(StorageLocation type, Path file) { + switch (type) { + case USER: + if (this.userWatcher != null) { + this.userWatcher.recordChange(file.getFileName().toString()); + } + break; + case GROUP: + if (this.groupWatcher != null) { + this.groupWatcher.recordChange(file.getFileName().toString()); + } + break; + case TRACK: + if (this.trackWatcher != null) { + this.trackWatcher.recordChange(file.getFileName().toString()); + } + break; + default: + throw new RuntimeException(); + } + } + + @Override + public void init() throws IOException { + super.init(); + + this.usersDirectory = Files.createDirectory(super.dataDirectory.resolve("users")); + this.groupsDirectory = Files.createDirectory(super.dataDirectory.resolve("groups")); + this.tracksDirectory = Files.createDirectory(super.dataDirectory.resolve("tracks")); + + // Listen for file changes. + FileWatcher watcher = this.plugin.getFileWatcher().orElse(null); + if (watcher != null) { + this.userWatcher = watcher.getWatcher(this.usersDirectory); + this.userWatcher.addListener(path -> { + String s = path.getFileName().toString(); + + if (!s.endsWith(this.fileExtension)) { + return; + } + + String user = s.substring(0, s.length() - this.fileExtension.length()); + UUID uuid = Uuids.parseNullable(user); + if (uuid == null) { + return; + } + + User u = this.plugin.getUserManager().getIfLoaded(uuid); + if (u != null) { + this.plugin.getLogger().info("[FileWatcher] Detected change in user file for " + u.getFriendlyName() + " - reloading..."); + this.plugin.getStorage().loadUser(uuid, null); + } + }); + + this.groupWatcher = watcher.getWatcher(this.groupsDirectory); + this.groupWatcher.addListener(path -> { + String s = path.getFileName().toString(); + + if (!s.endsWith(this.fileExtension)) { + return; + } + + String groupName = s.substring(0, s.length() - this.fileExtension.length()); + this.plugin.getLogger().info("[FileWatcher] Detected change in group file for " + groupName + " - reloading..."); + this.plugin.getUpdateTaskBuffer().request(); + }); + + this.trackWatcher = watcher.getWatcher(this.tracksDirectory); + this.trackWatcher.addListener(path -> { + String s = path.getFileName().toString(); + + if (!s.endsWith(this.fileExtension)) { + return; + } + + String trackName = s.substring(0, s.length() - this.fileExtension.length()); + this.plugin.getLogger().info("[FileWatcher] Detected change in track file for " + trackName + " - reloading..."); + this.plugin.getStorage().loadAllTracks(); + }); + } + } + + @Override + public void applyBulkUpdate(BulkUpdate bulkUpdate) throws Exception { + if (bulkUpdate.getDataType().isIncludingUsers()) { + try (Stream s = Files.list(getDirectory(StorageLocation.USER))) { + s.filter(getFileTypeFilter()).forEach(file -> { + try { + registerFileAction(StorageLocation.USER, file); + ConfigurationNode object = readFile(file); + ConfigurationNode results = processBulkUpdate(bulkUpdate, object); + if (results != null) { + saveFile(file, object); + } + } catch (Exception e) { + throw reportException(file.getFileName().toString(), e); + } + }); + } + } + + if (bulkUpdate.getDataType().isIncludingGroups()) { + try (Stream s = Files.list(getDirectory(StorageLocation.GROUP))) { + s.filter(getFileTypeFilter()).forEach(file -> { + try { + registerFileAction(StorageLocation.GROUP, file); + ConfigurationNode object = readFile(file); + ConfigurationNode results = processBulkUpdate(bulkUpdate, object); + if (results != null) { + saveFile(file, object); + } + } catch (Exception e) { + throw reportException(file.getFileName().toString(), e); + } + }); + } + } + } + + @Override + public Set getUniqueUsers() throws IOException { + try (Stream stream = Files.list(this.usersDirectory)) { + return stream.filter(getFileTypeFilter()) + .map(p -> p.getFileName().toString()) + .map(s -> s.substring(0, s.length() - this.fileExtension.length())) + .map(UUID::fromString) + .collect(Collectors.toSet()); + } + } + + @Override + public List> getUsersWithPermission(String permission) throws Exception { + List> held = new ArrayList<>(); + try (Stream stream = Files.list(getDirectory(StorageLocation.USER))) { + stream.filter(getFileTypeFilter()) + .forEach(file -> { + String fileName = file.getFileName().toString(); + try { + registerFileAction(StorageLocation.USER, file); + ConfigurationNode object = readFile(file); + UUID holder = UUID.fromString(fileName.substring(0, fileName.length() - this.fileExtension.length())); + Set nodes = readNodes(object); + for (NodeModel e : nodes) { + if (!e.getPermission().equalsIgnoreCase(permission)) { + continue; + } + held.add(NodeHeldPermission.of(holder, e)); + } + } catch (Exception e) { + throw reportException(file.getFileName().toString(), e); + } + }); + } + return held; + } + + @Override + public void loadAllGroups() throws IOException { + List groups; + try (Stream stream = Files.list(this.groupsDirectory)) { + groups = stream.filter(getFileTypeFilter()) + .map(p -> p.getFileName().toString()) + .map(s -> s.substring(0, s.length() - this.fileExtension.length())) + .collect(Collectors.toList()); + } + + boolean success = true; + for (String g : groups) { + try { + loadGroup(g); + } catch (Exception e) { + e.printStackTrace(); + success = false; + } + } + + if (!success) { + throw new RuntimeException("Exception occurred whilst loading a group"); + } + + GroupManager gm = this.plugin.getGroupManager(); + gm.getAll().values().stream() + .filter(g -> !groups.contains(g.getName())) + .forEach(gm::unload); + } + + @Override + public List> getGroupsWithPermission(String permission) throws Exception { + List> held = new ArrayList<>(); + try (Stream stream = Files.list(getDirectory(StorageLocation.USER))) { + stream.filter(getFileTypeFilter()) + .forEach(file -> { + String fileName = file.getFileName().toString(); + try { + registerFileAction(StorageLocation.GROUP, file); + ConfigurationNode object = readFile(file); + String holder = fileName.substring(0, fileName.length() - this.fileExtension.length()); + Set nodes = readNodes(object); + for (NodeModel e : nodes) { + if (!e.getPermission().equalsIgnoreCase(permission)) { + continue; + } + held.add(NodeHeldPermission.of(holder, e)); + } + } catch (Exception e) { + throw reportException(file.getFileName().toString(), e); + } + }); + } + return held; + } + + @Override + public void loadAllTracks() throws IOException { + List tracks; + try (Stream stream = Files.list(this.tracksDirectory)) { + tracks = stream.filter(getFileTypeFilter()) + .map(p -> p.getFileName().toString()) + .map(s -> s.substring(0, s.length() - this.fileExtension.length())) + .collect(Collectors.toList()); + } + + boolean success = true; + for (String t : tracks) { + try { + loadTrack(t); + } catch (Exception e) { + e.printStackTrace(); + success = false; + } + } + + if (!success) { + throw new RuntimeException("Exception occurred whilst loading a track"); + } + + TrackManager tm = this.plugin.getTrackManager(); + tm.getAll().values().stream() + .filter(t -> !tracks.contains(t.getName())) + .forEach(tm::unload); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/ConfigurateLoader.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/ConfigurateLoader.java new file mode 100644 index 00000000..36aad315 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/ConfigurateLoader.java @@ -0,0 +1,40 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.storage.dao.file.loader; + +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.loader.ConfigurationLoader; + +import java.nio.file.Path; + +/** + * Wraps an object which can produce configurate {@link ConfigurationLoader}s. + */ +public interface ConfigurateLoader { + + ConfigurationLoader loader(Path path); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/HoconDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/HoconLoader.java similarity index 82% rename from common/src/main/java/me/lucko/luckperms/common/storage/dao/file/HoconDao.java rename to common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/HoconLoader.java index cd40452b..d62fee9b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/HoconDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/HoconLoader.java @@ -23,9 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.storage.dao.file; - -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +package me.lucko.luckperms.common.storage.dao.file.loader; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.hocon.HoconConfigurationLoader; @@ -35,13 +33,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -public class HoconDao extends ConfigurateDao { - public HoconDao(LuckPermsPlugin plugin, String dataFolderName) { - super(plugin, "HOCON", ".conf", dataFolderName); - } +public class HoconLoader implements ConfigurateLoader { @Override - protected ConfigurationLoader loader(Path path) { + public ConfigurationLoader loader(Path path) { return HoconConfigurationLoader.builder() .setSource(() -> Files.newBufferedReader(path, StandardCharsets.UTF_8)) .setSink(() -> Files.newBufferedWriter(path, StandardCharsets.UTF_8)) diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/JsonDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/JsonLoader.java similarity index 83% rename from common/src/main/java/me/lucko/luckperms/common/storage/dao/file/JsonDao.java rename to common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/JsonLoader.java index ce8af824..1d280a17 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/JsonDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/JsonLoader.java @@ -23,9 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.storage.dao.file; - -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +package me.lucko.luckperms.common.storage.dao.file.loader; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.gson.GsonConfigurationLoader; @@ -35,13 +33,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -public class JsonDao extends ConfigurateDao { - public JsonDao(LuckPermsPlugin plugin, String dataFolderName) { - super(plugin, "JSON", ".json", dataFolderName); - } +public class JsonLoader implements ConfigurateLoader { @Override - protected ConfigurationLoader loader(Path path) { + public ConfigurationLoader loader(Path path) { return GsonConfigurationLoader.builder() .setIndent(2) .setSource(() -> Files.newBufferedReader(path, StandardCharsets.UTF_8)) diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/YamlDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/YamlLoader.java similarity index 83% rename from common/src/main/java/me/lucko/luckperms/common/storage/dao/file/YamlDao.java rename to common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/YamlLoader.java index fe299805..0d950ba6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/YamlDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/loader/YamlLoader.java @@ -23,9 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.storage.dao.file; - -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +package me.lucko.luckperms.common.storage.dao.file.loader; import org.yaml.snakeyaml.DumperOptions; @@ -37,13 +35,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -public class YamlDao extends ConfigurateDao { - public YamlDao(LuckPermsPlugin plugin, String dataFolderName) { - super(plugin, "YAML", ".yml", dataFolderName); - } +public class YamlLoader implements ConfigurateLoader { @Override - protected ConfigurationLoader loader(Path path) { + public ConfigurationLoader loader(Path path) { return YAMLConfigurationLoader.builder() .setFlowStyle(DumperOptions.FlowStyle.BLOCK) .setIndent(2) diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/SqlDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/SqlDao.java index 2248621d..358d593d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/SqlDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/SqlDao.java @@ -151,63 +151,56 @@ public class SqlDao extends AbstractDao { } @Override - public void init() { - try { - this.provider.init(); + public void init() throws Exception { + this.provider.init(); - // Init tables - if (!tableExists(this.prefix.apply("{prefix}user_permissions"))) { - String schemaFileName = "me/lucko/luckperms/schema/" + this.provider.getName().toLowerCase() + ".sql"; - try (InputStream is = this.plugin.getBootstrap().getResourceStream(schemaFileName)) { - if (is == null) { - throw new Exception("Couldn't locate schema file for " + this.provider.getName()); - } - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - try (Connection connection = this.provider.getConnection()) { - try (Statement s = connection.createStatement()) { - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("--") || line.startsWith("#")) continue; - - sb.append(line); - - // check for end of declaration - if (line.endsWith(";")) { - sb.deleteCharAt(sb.length() - 1); - - String result = this.prefix.apply(sb.toString().trim()); - if (!result.isEmpty()) s.addBatch(result); - - // reset - sb = new StringBuilder(); - } - } - s.executeBatch(); - } - } - } + // Init tables + if (!tableExists(this.prefix.apply("{prefix}user_permissions"))) { + String schemaFileName = "me/lucko/luckperms/schema/" + this.provider.getName().toLowerCase() + ".sql"; + try (InputStream is = this.plugin.getBootstrap().getResourceStream(schemaFileName)) { + if (is == null) { + throw new Exception("Couldn't locate schema file for " + this.provider.getName()); } - } - // migrations - try { - if (!(this.provider instanceof SQLiteConnectionFactory) && !(this.provider instanceof PostgreConnectionFactory)) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { try (Connection connection = this.provider.getConnection()) { try (Statement s = connection.createStatement()) { - s.execute(this.prefix.apply("ALTER TABLE {prefix}actions MODIFY COLUMN actor_name VARCHAR(100)")); - s.execute(this.prefix.apply("ALTER TABLE {prefix}actions MODIFY COLUMN action VARCHAR(300)")); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("--") || line.startsWith("#")) continue; + + sb.append(line); + + // check for end of declaration + if (line.endsWith(";")) { + sb.deleteCharAt(sb.length() - 1); + + String result = this.prefix.apply(sb.toString().trim()); + if (!result.isEmpty()) s.addBatch(result); + + // reset + sb = new StringBuilder(); + } + } + s.executeBatch(); } } } - } catch (Exception e) { - e.printStackTrace(); } + } - + // migrations + try { + if (!(this.provider instanceof SQLiteConnectionFactory) && !(this.provider instanceof PostgreConnectionFactory)) { + try (Connection connection = this.provider.getConnection()) { + try (Statement s = connection.createStatement()) { + s.execute(this.prefix.apply("ALTER TABLE {prefix}actions MODIFY COLUMN actor_name VARCHAR(100)")); + s.execute(this.prefix.apply("ALTER TABLE {prefix}actions MODIFY COLUMN action VARCHAR(300)")); + } + } + } } catch (Exception e) { - this.plugin.getLogger().severe("Error occurred whilst initialising the database."); e.printStackTrace(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/FlatfileConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/FlatfileConnectionFactory.java index 30cbb498..b1702372 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/FlatfileConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/FlatfileConnectionFactory.java @@ -27,7 +27,9 @@ package me.lucko.luckperms.common.storage.dao.sql.connection.file; import me.lucko.luckperms.common.storage.dao.sql.connection.AbstractConnectionFactory; -import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.DecimalFormat; import java.util.LinkedHashMap; import java.util.Map; @@ -35,9 +37,9 @@ import java.util.Map; abstract class FlatfileConnectionFactory extends AbstractConnectionFactory { protected static final DecimalFormat DF = new DecimalFormat("#.##"); - protected final File file; + protected final Path file; - FlatfileConnectionFactory(String name, File file) { + FlatfileConnectionFactory(String name, Path file) { super(name); this.file = file; } @@ -47,7 +49,7 @@ abstract class FlatfileConnectionFactory extends AbstractConnectionFactory { } - protected File getWriteFile() { + protected Path getWriteFile() { return this.file; } @@ -55,9 +57,16 @@ abstract class FlatfileConnectionFactory extends AbstractConnectionFactory { public Map getMeta() { Map ret = new LinkedHashMap<>(); - File databaseFile = getWriteFile(); - if (databaseFile.exists()) { - double size = databaseFile.length() / 1048576D; + Path databaseFile = getWriteFile(); + if (Files.exists(databaseFile)) { + long length; + try { + length = Files.size(databaseFile); + } catch (IOException e) { + length = 0; + } + + double size = length / 1048576D; ret.put("File Size", DF.format(size) + "MB"); } else { ret.put("File Size", "0MB"); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/H2ConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/H2ConnectionFactory.java index 09dabe81..3b1acd71 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/H2ConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/H2ConnectionFactory.java @@ -29,9 +29,11 @@ import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import java.io.File; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.Connection; import java.sql.Driver; import java.sql.SQLException; @@ -45,13 +47,17 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory { // the active connection private NonClosableConnection connection; - public H2ConnectionFactory(LuckPermsPlugin plugin, File file) { + public H2ConnectionFactory(LuckPermsPlugin plugin, Path file) { super("H2", file); // backwards compat - File data = new File(file.getParent(), "luckperms.db.mv.db"); - if (data.exists()) { - data.renameTo(new File(file.getParent(), file.getName() + ".mv.db")); + Path data = file.getParent().resolve("luckperms.db.mv.db"); + if (Files.exists(data)) { + try { + Files.move(data, getWriteFile()); + } catch (IOException e) { + e.printStackTrace(); + } } // setup the classloader @@ -68,7 +74,7 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory { @Override public synchronized Connection getConnection() throws SQLException { if (this.connection == null || this.connection.isClosed()) { - Connection connection = this.driver.connect("jdbc:h2:" + this.file.getAbsolutePath(), new Properties()); + Connection connection = this.driver.connect("jdbc:h2:" + this.file.toString(), new Properties()); if (connection != null) { this.connection = NonClosableConnection.wrap(connection); } @@ -89,8 +95,8 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory { } @Override - protected File getWriteFile() { + protected Path getWriteFile() { // h2 appends this to the end of the database file - return new File(super.file.getParent(), super.file.getName() + ".mv.db"); + return super.file.getParent().resolve(super.file.getFileName().toString() + ".mv.db"); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/SQLiteConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/SQLiteConnectionFactory.java index 29e3af72..e3180034 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/SQLiteConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/sql/connection/file/SQLiteConnectionFactory.java @@ -29,9 +29,11 @@ import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import java.io.File; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.Connection; import java.sql.SQLException; import java.util.EnumSet; @@ -44,13 +46,17 @@ public class SQLiteConnectionFactory extends FlatfileConnectionFactory { // the active connection private NonClosableConnection connection; - public SQLiteConnectionFactory(LuckPermsPlugin plugin, File file) { + public SQLiteConnectionFactory(LuckPermsPlugin plugin, Path file) { super("SQLite", file); // backwards compat - File data = new File(file.getParent(), "luckperms.sqlite"); - if (data.exists()) { - data.renameTo(file); + Path data = file.getParent().resolve("luckperms.sqlite"); + if (Files.exists(data)) { + try { + Files.move(data, file); + } catch (IOException e) { + e.printStackTrace(); + } } // setup the classloader @@ -79,7 +85,7 @@ public class SQLiteConnectionFactory extends FlatfileConnectionFactory { @Override public synchronized Connection getConnection() throws SQLException { if (this.connection == null || this.connection.isClosed()) { - Connection connection = createConnection("jdbc:sqlite:" + this.file.getAbsolutePath()); + Connection connection = createConnection("jdbc:sqlite:" + this.file.toString()); if (connection != null) { this.connection = NonClosableConnection.wrap(connection); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUtils.java b/common/src/main/java/me/lucko/luckperms/common/utils/MoreFiles.java similarity index 57% rename from common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUtils.java rename to common/src/main/java/me/lucko/luckperms/common/utils/MoreFiles.java index 6e176e28..ceddcb63 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileUtils.java +++ b/common/src/main/java/me/lucko/luckperms/common/utils/MoreFiles.java @@ -23,43 +23,20 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.storage.dao.file; +package me.lucko.luckperms.common.utils; -import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; -public final class FileUtils { +public final class MoreFiles { - public static File mkdir(File file) throws IOException { - if (file.exists()) { - return file; + public static Path createFileIfNotExists(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createFile(path); } - if (!file.mkdir()) { - throw new IOException("Unable to create directory - " + file.getPath()); - } - return file; + return path; } - public static File mkdirs(File file) throws IOException { - if (file.exists()) { - return file; - } - if (!file.mkdirs()) { - throw new IOException("Unable to create directory - " + file.getPath()); - } - return file; - } - - public static File createNewFile(File file) throws IOException { - if (file.exists()) { - return file; - } - if (!file.createNewFile()) { - throw new IOException("Unable to create file - " + file.getPath()); - } - return file; - } - - private FileUtils() {} - + private MoreFiles() {} } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java index 2345b4b4..7a0bb597 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java @@ -33,15 +33,13 @@ import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import cn.nukkit.Player; import cn.nukkit.plugin.PluginBase; -import java.io.File; import java.io.InputStream; +import java.nio.file.Path; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; -import javax.annotation.Nullable; - /** * Bootstrap plugin for LuckPerms running on Nukkit. */ @@ -160,8 +158,8 @@ public class LPNukkitBootstrap extends PluginBase implements LuckPermsBootstrap } @Override - public File getDataDirectory() { - return getDataFolder(); + public Path getDataDirectory() { + return getDataFolder().toPath().toAbsolutePath(); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java index d185e1b0..f09765bb 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java @@ -51,7 +51,6 @@ import org.spongepowered.api.scheduler.Scheduler; import org.spongepowered.api.scheduler.SpongeExecutorService; import org.spongepowered.api.scheduler.SynchronousExecutor; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -61,8 +60,6 @@ import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; -import javax.annotation.Nullable; - /** * Bootstrap plugin for LuckPerms running on Sponge. */ @@ -242,19 +239,19 @@ public class LPSpongeBootstrap implements LuckPermsBootstrap { } @Override - public File getDataDirectory() { + public Path getDataDirectory() { Path dataDirectory = this.game.getGameDirectory().resolve("luckperms"); try { Files.createDirectories(dataDirectory); } catch (IOException e) { e.printStackTrace(); } - return dataDirectory.toFile(); + return dataDirectory.toAbsolutePath(); } @Override - public File getConfigDirectory() { - return this.configDirectory.toFile(); + public Path getConfigDirectory() { + return this.configDirectory.toAbsolutePath(); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java index 790b6eb6..41b202dd 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java @@ -54,7 +54,6 @@ import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.text.Text; -import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -110,7 +109,7 @@ public class LuckPermsService implements LPPermissionService { this.permissionDescriptions = new ConcurrentHashMap<>(); // init subject storage - this.storage = new SubjectStorage(this, new File(plugin.getBootstrap().getDataDirectory(), "sponge-data")); + this.storage = new SubjectStorage(this, plugin.getBootstrap().getDataDirectory().resolve("sponge-data")); // load defaults collection this.defaultSubjects = new DefaultsCollection(this); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/SubjectStorage.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/SubjectStorage.java index 7b3f9c96..7e4b8c01 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/SubjectStorage.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/persisted/SubjectStorage.java @@ -34,15 +34,16 @@ import me.lucko.luckperms.sponge.service.model.LPPermissionService; import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Handles persisted Subject I/O and (de)serialization @@ -62,9 +63,9 @@ public class SubjectStorage { /** * The root directory used to store files */ - private final File container; + private final Path container; - public SubjectStorage(LPPermissionService service, File container) { + public SubjectStorage(LPPermissionService service, Path container) { this.service = service; this.gson = new GsonBuilder().setPrettyPrinting().create(); this.container = container; @@ -72,7 +73,11 @@ public class SubjectStorage { } private void checkContainer() { - this.container.getParentFile().mkdirs(); + try { + Files.createDirectories(this.container.getParent()); + } catch (IOException e) { + e.printStackTrace(); + } } /** @@ -83,12 +88,14 @@ public class SubjectStorage { public Set getSavedCollections() { checkContainer(); - File[] dirs = this.container.listFiles(File::isDirectory); - if (dirs == null) { - return Collections.emptySet(); + try (Stream s = Files.list(this.container)) { + return s.filter(p -> Files.isDirectory(p)) + .map(p -> p.getFileName().toString()) + .collect(Collectors.toSet()); + } catch (IOException e) { + e.printStackTrace(); + return ImmutableSet.of(); } - - return ImmutableSet.copyOf(dirs).stream().map(File::getName).collect(Collectors.toSet()); } /** @@ -98,14 +105,16 @@ public class SubjectStorage { * @param subjectIdentifier the identifier of the subject * @return a file */ - private File resolveFile(String collectionIdentifier, String subjectIdentifier) { + private Path resolveFile(String collectionIdentifier, String subjectIdentifier) { checkContainer(); - File collection = new File(this.container, collectionIdentifier); - if (!collection.exists()) { - collection.mkdirs(); + Path collection = this.container.resolve(collectionIdentifier); + try { + Files.createDirectories(collection); + } catch (IOException e) { + e.printStackTrace(); } - return new File(collection, subjectIdentifier + ".json"); + return collection.resolve(subjectIdentifier + ".json"); } /** @@ -115,7 +124,7 @@ public class SubjectStorage { * @throws IOException if the write fails */ public void saveToFile(PersistedSubject subject) throws IOException { - File subjectFile = resolveFile(subject.getParentCollection().getIdentifier(), subject.getIdentifier()); + Path subjectFile = resolveFile(subject.getParentCollection().getIdentifier(), subject.getIdentifier()); saveToFile(SubjectDataContainer.copyOf(subject.getSubjectData()), subjectFile); } @@ -126,14 +135,9 @@ public class SubjectStorage { * @param file the file * @throws IOException if the write fails */ - public void saveToFile(SubjectDataContainer container, File file) throws IOException { - file.getParentFile().mkdirs(); - if (file.exists()) { - file.delete(); - } - file.createNewFile(); - - try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + public void saveToFile(SubjectDataContainer container, Path file) throws IOException { + Files.createDirectories(file.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { this.gson.toJson(container.serialize(), writer); writer.flush(); } @@ -147,28 +151,27 @@ public class SubjectStorage { */ public Map loadAllFromFile(String collectionIdentifier) { checkContainer(); - File collection = new File(this.container, collectionIdentifier); - if (!collection.exists()) { + Path collection = this.container.resolve(collectionIdentifier); + if (!Files.exists(collection)) { return Collections.emptyMap(); } - String[] fileNames = collection.list((dir, name) -> name.endsWith(".json")); - if (fileNames == null) return Collections.emptyMap(); - Map holders = new HashMap<>(); - for (String name : fileNames) { - File subjectFile = new File(collection, name); - - try { - LoadedSubject s = loadFromFile(subjectFile); - if (s != null) { - holders.put(s.identifier, s.data); - } - } catch (IOException e) { - e.printStackTrace(); - } + try (Stream s = Files.list(collection)){ + s.filter(p -> p.getFileName().toString().endsWith(".json")) + .forEach(subjectFile -> { + try { + LoadedSubject sub = loadFromFile(subjectFile); + if (sub != null) { + holders.put(sub.identifier, sub.data); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + } catch (IOException e) { + e.printStackTrace(); } - return holders; } @@ -182,12 +185,12 @@ public class SubjectStorage { */ public LoadedSubject loadFromFile(String collectionIdentifier, String subjectIdentifier) throws IOException { checkContainer(); - File collection = new File(this.container, collectionIdentifier); - if (!collection.exists()) { + Path collection = this.container.resolve(collectionIdentifier); + if (!Files.exists(collection)) { return null; } - File subject = new File(collection, subjectIdentifier + ".json"); + Path subject = collection.resolve(subjectIdentifier + ".json"); return new LoadedSubject(subjectIdentifier, loadFromFile(subject).data); } @@ -198,14 +201,15 @@ public class SubjectStorage { * @return a loaded subject * @throws IOException if the read fails */ - public LoadedSubject loadFromFile(File file) throws IOException { - if (!file.exists()) { + public LoadedSubject loadFromFile(Path file) throws IOException { + if (!Files.exists(file)) { return null; } - String subjectName = file.getName().substring(0, file.getName().length() - ".json".length()); + String fileName = file.getFileName().toString(); + String subjectName = fileName.substring(0, fileName.length() - ".json".length()); - try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { JsonObject data = this.gson.fromJson(reader, JsonObject.class); SubjectDataContainer model = SubjectDataContainer.derserialize(this.service, data); return new LoadedSubject(subjectName, model);