Merge pull request #2900 from Multiverse/better-revamp-config

refactor: Abstract handle class and improve node handling
This commit is contained in:
Jeremy Wood 2023-03-30 01:43:41 -04:00 committed by GitHub
commit aac01b9e7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 813 additions and 646 deletions

View File

@ -2,6 +2,7 @@ package com.onarandombox.MultiverseCore.api;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.placeholders.MultiverseCorePlaceholders;
import io.vavr.control.Try;
import org.jvnet.hk2.annotations.Contract;
@Contract
@ -34,19 +35,21 @@ public interface MVConfig {
/**
* Gets a property from the config.
*
* @param name The name of the property.
* @return The value of the property.
* @param name The name of the property.
* @return A {@link Try} with the value of the property, otherwise a {@link Try.Failure} if there is no property by
* that name.
*/
Object getProperty(String name);
Try<Object> getProperty(String name);
/**
* Sets a property in the config.
*
* @param name The name of the property.
* @param value The value of the property.
* @return True if the property was set successfully.
* @return An empty {@link Try} if the property was set successfully, otherwise a {@link Try.Failure} with the
* exception explaining why the property could not be set.
*/
boolean setProperty(String name, Object value);
Try<Void> setProperty(String name, Object value);
/**
* Sets world access permissions should be enforced.

View File

@ -1,6 +1,5 @@
package com.onarandombox.MultiverseCore.commands;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
@ -9,11 +8,12 @@ import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.onarandombox.MultiverseCore.api.MVConfig;
import com.onarandombox.MultiverseCore.commandtools.MVCommandIssuer;
import com.onarandombox.MultiverseCore.commandtools.MVCommandManager;
import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand;
import com.onarandombox.MultiverseCore.commandtools.context.MVConfigValue;
import com.onarandombox.MultiverseCore.config.MVCoreConfig;
import com.onarandombox.MultiverseCore.exceptions.MultiverseException;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@ -35,7 +35,7 @@ public class ConfigCommand extends MultiverseCommand {
@CommandCompletion("@mvconfigs")
@Syntax("<name> [new-value]")
@Description("") //TODO
public void onConfigCommand(BukkitCommandIssuer issuer,
public void onConfigCommand(MVCommandIssuer issuer,
@Syntax("<name>")
@Description("") //TODO
@ -54,21 +54,26 @@ public class ConfigCommand extends MultiverseCommand {
updateConfigValue(issuer, name, value.getValue());
}
private void showConfigValue(BukkitCommandIssuer issuer, String name) {
Object currentValue = config.getProperty(name);
if (currentValue == null) {
issuer.sendMessage("No such config option: " + name);
return;
}
issuer.sendMessage(name + "is currently set to " + config.getProperty(name));
private void showConfigValue(MVCommandIssuer issuer, String name) {
config.getProperty(name)
.onSuccess(value -> issuer.sendMessage(name + "is currently set to " + value))
.onFailure(e -> issuer.sendMessage("Unable to get " + name + ": " + e.getMessage()));
}
private void updateConfigValue(BukkitCommandIssuer issuer, String name, Object value) {
if (!config.setProperty(name, value)) {
issuer.sendMessage("Unable to set " + name + " to " + value);
return;
}
config.save();
issuer.sendMessage("Successfully set " + name + " to " + value);
private void updateConfigValue(MVCommandIssuer issuer, String name, Object value) {
config.setProperty(name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage("Successfully set " + name + " to " + value);
})
.onFailure(e -> {
issuer.sendMessage("Unable to set " + name + " to " + value + ".");
if (e instanceof MultiverseException) {
var message = ((MultiverseException) e).getMVMessage();
if (message != null) {
issuer.sendError(message);
}
}
});
}
}

View File

@ -15,14 +15,15 @@ import com.onarandombox.MultiverseCore.api.MVWorldManager;
import com.onarandombox.MultiverseCore.commandtools.context.GameRuleValue;
import com.onarandombox.MultiverseCore.commandtools.context.MVConfigValue;
import com.onarandombox.MultiverseCore.config.MVCoreConfig;
import com.onarandombox.MultiverseCore.configuration.node.Node;
import com.onarandombox.MultiverseCore.configuration.node.ValueNode;
import com.onarandombox.MultiverseCore.destination.DestinationsProvider;
import com.onarandombox.MultiverseCore.destination.ParsedDestination;
import com.onarandombox.MultiverseCore.display.filters.ContentFilter;
import com.onarandombox.MultiverseCore.display.filters.DefaultContentFilter;
import com.onarandombox.MultiverseCore.display.filters.RegexContentFilter;
import com.onarandombox.MultiverseCore.utils.PlayerFinder;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
import io.github.townyadvanced.commentedconfiguration.setting.TypedValueNode;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.GameRule;
import org.bukkit.entity.Player;
@ -123,7 +124,7 @@ public class MVCommandContexts extends PaperCommandContexts {
if (Strings.isNullOrEmpty(configName)) {
throw new InvalidCommandArgument("No config name specified.");
}
Optional<CommentedNode> node = config.getNodes().findNode(configName);
Option<Node> node = config.getNodes().findNode(configName);
if (node.isEmpty()) {
throw new InvalidCommandArgument("The config " + configName + " is not valid.");
}
@ -133,12 +134,12 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("No config value specified.");
}
if (!(node.get() instanceof TypedValueNode)) {
if (!(node.get() instanceof ValueNode)) {
context.popFirstArg();
return new MVConfigValue(valueString);
}
ContextResolver<?, BukkitCommandExecutionContext> resolver = getResolver(((TypedValueNode<?>) node.get()).getType());
ContextResolver<?, BukkitCommandExecutionContext> resolver = getResolver(((ValueNode<?>) node.get()).getType());
if (resolver == null) {
context.popFirstArg();
return new MVConfigValue(valueString);

View File

@ -7,7 +7,7 @@ import java.nio.file.Path;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.MultiverseCore;
import com.onarandombox.MultiverseCore.api.MVConfig;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import com.onarandombox.MultiverseCore.configuration.handle.CommentedYamlConfigHandle;
import com.onarandombox.MultiverseCore.configuration.migration.BooleanMigratorAction;
import com.onarandombox.MultiverseCore.configuration.migration.ConfigMigrator;
import com.onarandombox.MultiverseCore.configuration.migration.IntegerMigratorAction;
@ -15,6 +15,7 @@ import com.onarandombox.MultiverseCore.configuration.migration.InvertBoolMigrato
import com.onarandombox.MultiverseCore.configuration.migration.MoveMigratorAction;
import com.onarandombox.MultiverseCore.configuration.migration.VersionMigrator;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.NotNull;
@ -27,13 +28,13 @@ public class MVCoreConfig implements MVConfig {
private final Path configPath;
private final MVCoreConfigNodes configNodes;
private final ConfigHandle configHandle;
private final CommentedYamlConfigHandle configHandle;
@Inject
MVCoreConfig(@NotNull MultiverseCore core, @NotNull PluginManager pluginManager) {
this.configPath = Path.of(core.getDataFolder().getPath(), CONFIG_FILENAME);
this.configNodes = new MVCoreConfigNodes(pluginManager);
this.configHandle = ConfigHandle.builder(configPath)
this.configHandle = CommentedYamlConfigHandle.builder(configPath)
.logger(Logging.getLogger())
.nodes(configNodes.getNodes())
.migrator(ConfigMigrator.builder(configNodes.VERSION)
@ -110,12 +111,12 @@ public class MVCoreConfig implements MVConfig {
}
@Override
public Object getProperty(String name) {
public Try<Object> getProperty(String name) {
return configHandle.get(name);
}
@Override
public boolean setProperty(String name, Object value) {
public Try<Void> setProperty(String name, Object value) {
return configHandle.set(name, value);
}

View File

@ -1,14 +1,13 @@
package com.onarandombox.MultiverseCore.config;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.node.MVCommentedNode;
import com.onarandombox.MultiverseCore.configuration.node.MVValueNode;
import com.onarandombox.MultiverseCore.configuration.node.ConfigHeaderNode;
import com.onarandombox.MultiverseCore.configuration.node.ConfigNode;
import com.onarandombox.MultiverseCore.configuration.node.Node;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.event.MVDebugModeEvent;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
import jakarta.inject.Inject;
import org.bukkit.plugin.PluginManager;
import org.jvnet.hk2.annotations.Service;
class MVCoreConfigNodes {
@ -23,12 +22,12 @@ class MVCoreConfigNodes {
return nodes;
}
private <N extends CommentedNode> N node(N node) {
private <N extends Node> N node(N node) {
nodes.add(node);
return node;
}
private final MVCommentedNode HEADER = node(MVCommentedNode.builder("world") // TODO hacky way to get the header to the top of the file
private final ConfigHeaderNode HEADER = node(ConfigHeaderNode.builder("world") // TODO hacky way to get the header to the top of the file
.comment("####################################################################################################")
.comment("# #")
.comment("# █▀▄▀█ █░█ █░░ ▀█▀ █ █░█ █▀▀ █▀█ █▀ █▀▀   █▀▀ █▀█ █▀█ █▀▀ #")
@ -52,12 +51,12 @@ class MVCoreConfigNodes {
.comment("")
.build());
// private final MVCommentedNode WORLD_HEADER = node(MVCommentedNode.builder("world")
// private final ConfigHeaderNode WORLD_HEADER = node(ConfigHeaderNode.builder("world")
// .comment("")
// .comment("")
// .build());
public final MVValueNode<Boolean> ENFORCE_ACCESS = node(MVValueNode.builder("world.enforce-access", Boolean.class)
public final ConfigNode<Boolean> ENFORCE_ACCESS = node(ConfigNode.builder("world.enforce-access", Boolean.class)
.comment("This setting will prevent players from entering worlds they don't have access to.")
.comment("If this is set to false, players will be able to enter any world they want.")
.comment("If this is set to true, players will only be able to enter worlds they have")
@ -66,7 +65,7 @@ class MVCoreConfigNodes {
.name("enforce-access")
.build());
public final MVValueNode<Boolean> ENFORCE_GAMEMODE = node(MVValueNode.builder("world.enforce-gamemode", Boolean.class)
public final ConfigNode<Boolean> ENFORCE_GAMEMODE = node(ConfigNode.builder("world.enforce-gamemode", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will should enforce gamemode on world change.")
.comment("If enabled, players will be forced into the gamemode of the world they are entering, unless they have")
@ -75,14 +74,14 @@ class MVCoreConfigNodes {
.name("enforce-gamemode")
.build());
public final MVValueNode<Boolean> AUTO_PURGE_ENTITIES = node(MVValueNode.builder("world.auto-purge-entities", Boolean.class)
public final ConfigNode<Boolean> AUTO_PURGE_ENTITIES = node(ConfigNode.builder("world.auto-purge-entities", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will purge mobs and entities with be automatically.")
.defaultValue(false)
.name("auto-purge-entities")
.build());
public final MVValueNode<Boolean> TELEPORT_INTERCEPT = node(MVValueNode.builder("world.teleport-intercept", Boolean.class)
public final ConfigNode<Boolean> TELEPORT_INTERCEPT = node(ConfigNode.builder("world.teleport-intercept", Boolean.class)
.comment("")
.comment("If this is set to true, Multiverse will enforce access permissions for all teleportation,")
.comment("including teleportation from other plugins.")
@ -90,12 +89,12 @@ class MVCoreConfigNodes {
.name("teleport-intercept")
.build());
private final MVCommentedNode SPAWN_HEADER = node(MVCommentedNode.builder("spawn")
private final ConfigHeaderNode SPAWN_HEADER = node(ConfigHeaderNode.builder("spawn")
.comment("")
.comment("")
.build());
public final MVValueNode<Boolean> FIRST_SPAWN_OVERRIDE = node(MVValueNode.builder("spawn.first-spawn-override", Boolean.class)
public final ConfigNode<Boolean> FIRST_SPAWN_OVERRIDE = node(ConfigNode.builder("spawn.first-spawn-override", Boolean.class)
.comment("Sets whether Multiverse will override the first spawn location of a world.")
.comment("If enabled, Multiverse will set the first spawn location of a world to the spawn location of the world.")
.comment("If disabled, it will default to server.properties settings.")
@ -103,7 +102,7 @@ class MVCoreConfigNodes {
.name("first-spawn-override")
.build());
public final MVValueNode<String> FIRST_SPAWN_LOCATION = node(MVValueNode.builder("spawn.first-spawn-location", String.class)
public final ConfigNode<String> FIRST_SPAWN_LOCATION = node(ConfigNode.builder("spawn.first-spawn-location", String.class)
.comment("")
.comment("Sets the world that Multiverse will use as the location for players that first join the server.")
.comment("This only applies if first-spawn-override is set to true.")
@ -111,19 +110,19 @@ class MVCoreConfigNodes {
.name("first-spawn-location")
.build());
private final MVCommentedNode PORTAL_HEADER = node(MVCommentedNode.builder("portal")
private final ConfigHeaderNode PORTAL_HEADER = node(ConfigHeaderNode.builder("portal")
.comment("")
.comment("")
.build());
public final MVValueNode<Boolean> USE_CUSTOM_PORTAL_SEARCH = node(MVValueNode.builder("portal.use-custom-portal-search", Boolean.class)
public final ConfigNode<Boolean> USE_CUSTOM_PORTAL_SEARCH = node(ConfigNode.builder("portal.use-custom-portal-search", Boolean.class)
.comment("This config option defines whether or not Multiverse should interfere with's Bukkit's default portal search radius.")
.comment("Setting it to false would mean you want to simply let Bukkit decides the search radius itself.")
.defaultValue(false)
.name("use-custom-portal-search")
.build());
public final MVValueNode<Integer> CUSTOM_PORTAL_SEARCH_RADIUS = node(MVValueNode.builder("portal.custom-portal-search-radius", Integer.class)
public final ConfigNode<Integer> CUSTOM_PORTAL_SEARCH_RADIUS = node(ConfigNode.builder("portal.custom-portal-search-radius", Integer.class)
.comment("")
.comment("This config option defines the search radius Multiverse should use when searching for a portal.")
.comment("This only applies if use-custom-portal-search is set to true.")
@ -132,19 +131,19 @@ class MVCoreConfigNodes {
.validator(value -> value >= 0)
.build());
private final MVCommentedNode MESSAGING_HEADER = node(MVCommentedNode.builder("messaging")
private final ConfigHeaderNode MESSAGING_HEADER = node(ConfigHeaderNode.builder("messaging")
.comment("")
.comment("")
.build());
public final MVValueNode<Boolean> ENABLE_CHAT_PREFIX = node(MVValueNode.builder("messaging.enable-chat-prefix", Boolean.class)
public final ConfigNode<Boolean> ENABLE_CHAT_PREFIX = node(ConfigNode.builder("messaging.enable-chat-prefix", Boolean.class)
.comment("This config option defines whether or not Multiverse should prefix the chat with the world name.")
.comment("This only applies if use-custom-portal-search is set to true.")
.defaultValue(false)
.name("enable-chat-prefix")
.build());
public final MVValueNode<String> CHAT_PREFIX_FORMAT = node(MVValueNode.builder("messaging.chat-prefix-format", String.class)
public final ConfigNode<String> CHAT_PREFIX_FORMAT = node(ConfigNode.builder("messaging.chat-prefix-format", String.class)
.comment("")
.comment("This config option defines the format Multiverse should use when prefixing the chat with the world name.")
.comment("This only applies if enable-chat-prefix is set to true.")
@ -152,7 +151,7 @@ class MVCoreConfigNodes {
.name("chat-prefix-format")
.build());
public final MVValueNode<Boolean> REGISTER_PAPI_HOOK = node(MVValueNode.builder("messaging.register-papi-hook", Boolean.class)
public final ConfigNode<Boolean> REGISTER_PAPI_HOOK = node(ConfigNode.builder("messaging.register-papi-hook", Boolean.class)
.comment("")
.comment("This config option defines whether or not Multiverse should register the PlaceholderAPI hook.")
.comment("This only applies if PlaceholderAPI is installed.")
@ -160,12 +159,12 @@ class MVCoreConfigNodes {
.name("register-papi-hook")
.build());
private final MVCommentedNode MISC_HEADER = node(MVCommentedNode.builder("misc")
private final ConfigHeaderNode MISC_HEADER = node(ConfigHeaderNode.builder("misc")
.comment("")
.comment("")
.build());
public final MVValueNode<Integer> GLOBAL_DEBUG = node(MVValueNode.builder("misc.global-debug", Integer.class)
public final ConfigNode<Integer> GLOBAL_DEBUG = node(ConfigNode.builder("misc.global-debug", Integer.class)
.comment("This is our debug flag to help identify issues with Multiverse.")
.comment("If you are having issues with Multiverse, please set this to 3 and then post your log to pastebin.com")
.comment("Otherwise, there's no need to touch this. If not instructed by a wiki page or developer.")
@ -185,7 +184,7 @@ class MVCoreConfigNodes {
})
.build());
public final MVValueNode<Boolean> SILENT_START = node(MVValueNode.builder("misc.silent-start", Boolean.class)
public final ConfigNode<Boolean> SILENT_START = node(ConfigNode.builder("misc.silent-start", Boolean.class)
.comment("")
.comment("If true, the startup console messages will no longer show.")
.defaultValue(false)
@ -193,14 +192,14 @@ class MVCoreConfigNodes {
.onSetValue((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.build());
public final MVValueNode<Boolean> SHOW_DONATION_MESSAGE = node(MVValueNode.builder("misc.show-donation-message", Boolean.class)
public final ConfigNode<Boolean> SHOW_DONATION_MESSAGE = node(ConfigNode.builder("misc.show-donation-message", Boolean.class)
.comment("")
.comment("If you don't want to donate, you can set this to false and Multiverse will stop nagging you.")
.defaultValue(true)
.name("show-donation-message")
.build());
public final MVValueNode<Double> VERSION = node(MVValueNode.builder("version", Double.class)
public final ConfigNode<Double> VERSION = node(ConfigNode.builder("version", Double.class)
.comment("")
.comment("")
.comment("This just signifies the version number so we can see what version of config you have.")

View File

@ -1,360 +0,0 @@
package com.onarandombox.MultiverseCore.configuration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.migration.ConfigMigrator;
import com.onarandombox.MultiverseCore.configuration.node.EnhancedValueNode;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import io.github.townyadvanced.commentedconfiguration.CommentedConfiguration;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
import io.github.townyadvanced.commentedconfiguration.setting.TypedValueNode;
import io.github.townyadvanced.commentedconfiguration.setting.ValueNode;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A class that makes use of CommentedConfiguration to provide a simple way to load and save with node objects.
*/
public class ConfigHandle {
/**
* A builder class for creating a ConfigHandle.
*
* @param configPath The path to the configuration file in string.
* @return A new Builder instance.
*/
public static Builder builder(String configPath) {
return new Builder(configPath);
}
/**
* A builder class for creating a ConfigHandle.
*
* @param configPath The path to the configuration file.
* @return A new Builder instance.
*/
public static Builder builder(Path configPath) {
return new Builder(configPath);
}
@NotNull
protected final Path configPath;
@Nullable
protected final Logger logger;
@NotNull
protected final NodeGroup nodes;
protected final ConfigMigrator migrator;
protected CommentedConfiguration config;
/**
* Creates a new MVSettings instance that makes use of CommentedConfiguration.
*
* @param configPath The path to the configuration file.
* @param logger The Logger to use for error messages.
* @param nodes All the node path and values for the configuration.
* @param migrator The migrator to use for migrating the configuration.
*/
protected ConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @NotNull NodeGroup nodes, ConfigMigrator migrator) {
this.configPath = configPath;
this.nodes = nodes;
this.logger = logger;
this.migrator = migrator;
}
/**
* Loads the configuration.
*
* @return True if the configuration was loaded successfully, false otherwise.
*/
public boolean load() {
if (!createConfigFile()) {
return false;
}
this.config = new CommentedConfiguration(configPath, logger);
if (!config.load()) {
return false;
}
migrateConfig();
parseAllNodes();
return true;
}
/**
* Create a new config file if file does not exist
*
* @return True if file exist or created successfully, otherwise false.
*/
protected boolean createConfigFile() {
File configFile = configPath.toFile();
if (configFile.exists()) {
return true;
}
try {
if (!configFile.createNewFile()) {
return false;
}
Logging.info("Created new config file: %s", configFile.getName());
} catch (IOException e) {
return false;
}
return true;
}
/**
* Migration of the configuration based on {@link ConfigMigrator}.
*/
protected void migrateConfig() {
migrator.migrate(this);
}
/**
* Adds default node values to the configuration if they are not already present.
*/
protected void parseAllNodes() {
CommentedConfiguration oldConfig = config;
this.config = new CommentedConfiguration(configPath, logger);
for (CommentedNode node : nodes) {
if (node.getComments().length > 0) {
config.addComment(node.getPath(), node.getComments());
}
if (node instanceof TypedValueNode typedNode) {
if (!set(typedNode, oldConfig.getObject(node.getPath(), typedNode.getType(), typedNode.getDefaultValue()))) {
Logging.warning("Invalid value for node: %s, resetting to default...", node.getPath());
setDefault(typedNode);
}
} else if (node instanceof ValueNode valueNode) {
if (!set(valueNode, oldConfig.get(node.getPath(), valueNode.getDefaultValue()))) {
Logging.warning("Invalid value for node: %s, resetting to default...", node.getPath());
setDefault(valueNode);
}
}
}
}
/**
* Saves the configuration.
*/
public void save() {
config.save();
}
/**
* Checks if the configuration is loaded.
*
* @return True if the configuration is loaded, false otherwise.
*/
public boolean isLoaded() {
return config != null;
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param node The node to get the value of.
* @return The value of the node.
*/
public Object get(@NotNull ValueNode node) {
return config.get(node.getPath(), node.getDefaultValue());
}
/**
* Get the value of the node by name.
*
* @param name The name of the node to get the value of.
* @return The value of the node.
*/
public Object get(@NotNull String name) {
return nodes.findNode(name)
.map(node -> (node instanceof ValueNode) ? get((ValueNode) node) : null)
.orElse(null);
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param node The node to get the value of.
* @param type The type of the node value.
* @param <T> The type of the node value.
* @return The value of the node.
*/
public <T> T get(@NotNull ValueNode node, Class<T> type) {
return config.getObject(node.getPath(), type, (T) node.getDefaultValue());
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param node The node to get the value of.
* @param <T> The type of the node value.
* @return The value of the node.
*/
public <T> T get(@NotNull TypedValueNode<T> node) {
return config.getObject(node.getPath(), node.getType(), node.getDefaultValue());
}
/**
* Set the value of the node by name.
*
* @param name The name of the node to set the value of.
* @param value The value to set.
*/
public boolean set(@NotNull String name, Object value) {
return nodes.findNode(name)
.map(node -> node instanceof ValueNode && set((ValueNode) node, value))
.orElse(false);
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param node The node to set the value of.
* @param value The value to set.
*/
public boolean set(@NotNull ValueNode node, Object value) {
if (node instanceof TypedValueNode typedValueNode) {
return set(typedValueNode, value);
}
config.set(node.getPath(), value);
return true;
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param node The node to set the value of.
* @param value The value to set.
* @param <T> The type of the node value.
*/
public <T> boolean set(@NotNull TypedValueNode<T> node, T value) {
if (node instanceof EnhancedValueNode<T> enhancedValueNode) {
return set(enhancedValueNode, value);
}
config.set(node.getPath(), value);
return true;
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param node The node to set the value of.
* @param value The value to set.
* @return True if the value was set, false otherwise.
* @param <T> The type of the node value.
*/
public <T> boolean set(@NotNull EnhancedValueNode<T> node, T value) {
if (!node.isValid(value)) {
return false;
}
T oldValue = get(node);
config.set(node.getPath(), value);
node.onSetValue(oldValue, get(node));
return true;
}
/**
* Sets the default value of a node.
*
* @param node The node to set the default value of.
*/
public void setDefault(@NotNull ValueNode node) {
config.set(node.getPath(), node.getDefaultValue());
}
/**
* Gets the inner configuration object.
*
* @return The configuration object.
*/
public @NotNull CommentedConfiguration getConfig() {
return config;
}
/**
* Gets the path of the configuration file.
*/
public static class Builder {
private final Path configPath;
private Logger logger;
private NodeGroup nodes;
private ConfigMigrator migrator;
/**
* Creates a new builder.
*
* @param configPath The path of the configuration file.
*/
public Builder(String configPath) {
this.configPath = Path.of(configPath);
}
/**
* Creates a new builder.
*
* @param configPath The path of the configuration file.
*/
public Builder(Path configPath) {
this.configPath = configPath;
}
/**
* Sets the logger to use.
*
* @param plugin The plugin to get the logger from.
* @return The builder.
*/
public Builder logger(@NotNull Plugin plugin) {
return logger(plugin.getLogger());
}
/**
* Sets the logger to use.
*
* @param logger The logger to use.
* @return The builder.
*/
public Builder logger(@Nullable Logger logger) {
this.logger = logger;
return this;
}
/**
* Sets the nodes to use.
*
* @param nodes The nodes to use.
* @return The builder.
*/
public Builder nodes(@Nullable NodeGroup nodes) {
this.nodes = nodes;
return this;
}
/**
* Sets the migrator to use.
*
* @param migrator The migrator to use.
* @return The builder.
*/
public Builder migrator(@Nullable ConfigMigrator migrator) {
this.migrator = migrator;
return this;
}
/**
* Builds the settings.
*
* @return The built settings.
*/
public ConfigHandle build() {
return new ConfigHandle(configPath, logger, nodes, migrator);
}
}
}

View File

@ -0,0 +1,93 @@
package com.onarandombox.MultiverseCore.configuration.handle;
import java.nio.file.Path;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.migration.ConfigMigrator;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.configuration.node.CommentedNode;
import com.onarandombox.MultiverseCore.configuration.node.ValueNode;
import io.github.townyadvanced.commentedconfiguration.CommentedConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Configuration handle for commented YAML files.
*/
public class CommentedYamlConfigHandle extends FileConfigHandle<CommentedConfiguration> {
/**
* Creates a new builder for a {@link CommentedYamlConfigHandle}.
*
* @param configPath The path to the config file.
* @return The builder.
*/
public static @NotNull Builder builder(@NotNull Path configPath) {
return new Builder(configPath);
}
protected CommentedYamlConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
super(configPath, logger, nodes, migrator);
}
/**
* {@inheritDoc}
*/
@Override
protected boolean loadConfigObject() {
config = new CommentedConfiguration(configPath, logger);
return config.load();
}
/**
* {@inheritDoc}
*/
@Override
protected void setUpNodes() {
if (nodes == null || nodes.isEmpty()) {
return;
}
CommentedConfiguration oldConfig = config;
this.config = new CommentedConfiguration(configPath, logger);
nodes.forEach(node -> {
if (node instanceof CommentedNode typedNode) {
if (typedNode.getComments().length > 0) {
config.addComment(typedNode.getPath(), typedNode.getComments());
}
}
if (node instanceof ValueNode valueNode) {
set(valueNode, oldConfig.getObject(valueNode.getPath(), valueNode.getType(), valueNode.getDefaultValue())).onFailure(e -> {
Logging.warning("Failed to set node " + valueNode.getPath() + " to " + valueNode.getDefaultValue());
setDefault(valueNode);
});
}
});
}
/**
* {@inheritDoc}
*/
@Override
public boolean save() {
config.save();
return true;
}
public static class Builder extends FileConfigHandle.Builder<CommentedConfiguration, Builder> {
protected Builder(@NotNull Path configPath) {
super(configPath);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull CommentedYamlConfigHandle build() {
return new CommentedYamlConfigHandle(configPath, logger, nodes, migrator);
}
}
}

View File

@ -0,0 +1,259 @@
package com.onarandombox.MultiverseCore.configuration.handle;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.migration.ConfigMigrator;
import com.onarandombox.MultiverseCore.configuration.node.ConfigNodeNotFoundException;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.configuration.node.ValueNode;
import com.onarandombox.MultiverseCore.exceptions.MultiverseException;
import io.vavr.control.Try;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Generic configuration handle for file based configurations.
* @param <C> The configuration type.
*/
abstract class FileConfigHandle<C extends FileConfiguration> {
protected final @NotNull Path configPath;
protected final @NotNull File configFile;
protected final @Nullable Logger logger;
protected final @Nullable NodeGroup nodes;
protected final @Nullable ConfigMigrator migrator;
protected C config;
protected FileConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
this.configPath = configPath;
this.configFile = configPath.toFile();
this.logger = logger;
this.nodes = nodes;
this.migrator = migrator;
}
/**
* Loads the configuration.
*
* @return True if the configuration was loaded successfully, false otherwise.
*/
public boolean load() {
if (!createConfigFile()) {
Logging.severe("Failed to create config file: %s", configFile.getName());
return false;
}
if (!loadConfigObject()) {
Logging.severe("Failed to load config file: %s", configFile.getName());
return false;
}
migrateConfig();
setUpNodes();
return true;
}
/**
* Create a new config file if file does not exist
*
* @return True if file exist or created successfully, otherwise false.
*/
protected boolean createConfigFile() {
if (configFile.exists()) {
return true;
}
try {
if (!configFile.createNewFile()) {
return false;
}
Logging.info("Created new config file: %s", configFile.getName());
} catch (IOException e) {
return false;
}
return true;
}
/**
* Loads the configuration object.
*
* @return True if the configuration was loaded successfully, false otherwise.
*/
protected abstract boolean loadConfigObject();
/**
* Migrates the configuration.
*/
protected void migrateConfig() {
if (migrator != null) {
migrator.migrate(config);
}
}
/**
* Sets up the nodes.
*/
protected abstract void setUpNodes();
/**
* Saves the configuration.
*/
public abstract boolean save();
/**
* Checks if the configuration is loaded.
*
* @return True if the configuration is loaded, false otherwise.
*/
public boolean isLoaded() {
return config != null;
}
/**
* Gets the configuration.
*
* @return The configuration.
*/
public C getConfig() {
return config;
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
* @param name The name of the node.
* @return The value of the node.
*/
public Try<Object> get(@Nullable String name) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.map(node -> get((ValueNode<Object>) node));
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param node The node to get the value of.
* @return The value of the node.
*/
public <T> T get(@NotNull ValueNode<T> node) {
return config.getObject(node.getPath(), node.getType(), node.getDefaultValue());
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param name The name of the node.
* @param value The value to set.
* @return True if the value was set, false otherwise.
*/
public Try<Void> set(@Nullable String name, Object value) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMap(node -> set(node, value));
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param node The node to set the value of.
* @param value The value to set.
* @return True if the value was set, false otherwise.
* @param <T> The type of the node value.
*/
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
if (!node.validate(value)) {
return Try.failure(new MultiverseException("Validation failed", null)); // TODO replace validation
}
T oldValue = get(node);
config.set(node.getPath(), value);
node.onSetValue(oldValue, get(node));
return Try.success(null);
}
/**
* Sets the default value of a node.
*
* @param node The node to set the default value of.
*/
public void setDefault(@NotNull ValueNode node) {
config.set(node.getPath(), node.getDefaultValue());
}
/**
* Builder for {@link FileConfigHandle}.
*
* @param <C> The configuration type.
* @param <B> The builder type.
*/
public static abstract class Builder<C extends FileConfiguration, B extends Builder<C, B>> {
protected @NotNull Path configPath;
protected @Nullable Logger logger;
protected @Nullable NodeGroup nodes;
protected @Nullable ConfigMigrator migrator;
protected Builder(@NotNull Path configPath) {
this.configPath = configPath;
}
/**
* Sets the logger.
*
* @param logger The logger.
* @return The builder.
*/
public B logger(@Nullable Logger logger) {
this.logger = logger;
return self();
}
/**
* Sets the logger.
*
* @param plugin The plugin to get the logger from.
* @return The builder.
*/
public B logger(Plugin plugin) {
this.logger = plugin.getLogger();
return self();
}
/**
* Sets the nodes.
*
* @param nodes The nodes.
* @return The builder.
*/
public B nodes(@Nullable NodeGroup nodes) {
this.nodes = nodes;
return self();
}
/**
* Sets the migrator.
*
* @param migrator The migrator.
* @return The builder.
*/
public B migrator(@Nullable ConfigMigrator migrator) {
this.migrator = migrator;
return self();
}
/**
* Builds the configuration handle.
*
* @return The configuration handle.
*/
public abstract @NotNull FileConfigHandle<C> build();
@SuppressWarnings("unchecked")
protected B self() {
return (B) this;
}
}
}

View File

@ -0,0 +1,97 @@
package com.onarandombox.MultiverseCore.configuration.handle;
import java.io.IOException;
import java.nio.file.Path;
import java.util.logging.Logger;
import com.onarandombox.MultiverseCore.configuration.migration.ConfigMigrator;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.configuration.node.ValueNode;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Configuration handle for YAML files.
*/
public class YamlConfigHandle extends FileConfigHandle<YamlConfiguration> {
/**
* Creates a new builder for {@link YamlConfigHandle}.
*
* @param configPath The path to the config file.
* @return The builder.
*/
public static @NotNull Builder<? extends Builder> builder(@NotNull Path configPath) {
return new Builder<>(configPath);
}
protected YamlConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
super(configPath, logger, nodes, migrator);
}
/**
* {@inheritDoc}
*/
@Override
protected boolean loadConfigObject() {
config = new YamlConfiguration();
try {
config.load(configFile);
} catch (IOException | InvalidConfigurationException e) {
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
protected void setUpNodes() {
if (nodes == null || nodes.isEmpty()) {
return;
}
YamlConfiguration oldConfig = config;
config = new YamlConfiguration();
nodes.forEach(node -> {
if (node instanceof ValueNode valueNode) {
set(valueNode, oldConfig.getObject(valueNode.getPath(), valueNode.getType(), valueNode.getDefaultValue()));
}
});
}
/**
* {@inheritDoc}
*/
@Override
public boolean save() {
try {
config.save(configFile);
} catch (IOException e) {
return false;
}
return true;
}
/**
* Builder for {@link YamlConfigHandle}.
* @param <B> The type of the builder.
*/
public static class Builder<B extends Builder<B>> extends FileConfigHandle.Builder<YamlConfiguration, B> {
protected Builder(@NotNull Path configPath) {
super(configPath);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull YamlConfigHandle build() {
return new YamlConfigHandle(configPath, logger, nodes, migrator);
}
}
}

View File

@ -2,7 +2,7 @@ package com.onarandombox.MultiverseCore.configuration.migration;
import co.aikar.commands.ACFUtil;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import org.bukkit.configuration.file.FileConfiguration;
/**
* Single migrator action that converts a string value to a boolean.
@ -20,9 +20,9 @@ public class BooleanMigratorAction implements MigratorAction {
}
@Override
public void migrate(ConfigHandle settings) {
settings.getConfig().set(path, ACFUtil.isTruthy(settings.getConfig().getString(path, "")));
Logging.info("Converted %s to boolean %s", path, settings.getConfig().getBoolean(path));
public void migrate(FileConfiguration config) {
config.set(path, ACFUtil.isTruthy(config.getString(path, "")));
Logging.info("Converted %s to boolean %s", path, config.getBoolean(path));
}
}

View File

@ -4,8 +4,9 @@ import java.util.ArrayList;
import java.util.List;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import com.onarandombox.MultiverseCore.configuration.node.ValueNode;
import io.github.townyadvanced.commentedconfiguration.setting.TypedValueNode;
import org.bukkit.configuration.file.FileConfiguration;
/**
* Helper class for migrating configs to the latest config version.
@ -19,14 +20,14 @@ public class ConfigMigrator {
* Default value should be the current latest version number.
* @return The builder instance.
*/
public static Builder builder(TypedValueNode<Double> versionNode) {
public static Builder builder(ValueNode<Double> versionNode) {
return new Builder(versionNode);
}
private final TypedValueNode<Double> versionNode;
private final ValueNode<Double> versionNode;
private final List<VersionMigrator> versionMigrators;
protected ConfigMigrator(TypedValueNode<Double> versionNode, List<VersionMigrator> versionMigrators) {
protected ConfigMigrator(ValueNode<Double> versionNode, List<VersionMigrator> versionMigrators) {
this.versionNode = versionNode;
this.versionMigrators = versionMigrators;
}
@ -34,25 +35,25 @@ public class ConfigMigrator {
/**
* Migrates the config to the latest version if necessary.
*
* @param settings The target settings instance to migrate.
* @param config The target settings instance to migrate.
*/
public void migrate(ConfigHandle settings) {
double versionNumber = settings.getConfig().getDouble(versionNode.getPath());
public void migrate(FileConfiguration config) {
double versionNumber = config.getDouble(versionNode.getPath());
for (VersionMigrator versionMigrator : versionMigrators) {
if (versionNumber < versionMigrator.getVersion()) {
Logging.info("Migrating config from version %s to %s...", versionNumber, versionMigrator.getVersion());
versionMigrator.migrate(settings);
versionMigrator.migrate(config);
}
}
// Set the version number to the latest version number
settings.setDefault(versionNode);
config.set(versionNode.getPath(), versionNode.getDefaultValue());
}
/**
* A builder for a ConfigMigrator.
*/
public static class Builder {
private final TypedValueNode<Double> versionNode;
private final ValueNode<Double> versionNode;
private final List<VersionMigrator> versionMigrators;
/**
@ -61,7 +62,7 @@ public class ConfigMigrator {
* @param versionNode The node that stores the version number of the config.
* Default value should be the current latest version number.
*/
public Builder(TypedValueNode<Double> versionNode) {
public Builder(ValueNode<Double> versionNode) {
this.versionNode = versionNode;
this.versionMigrators = new ArrayList<>();
}

View File

@ -2,8 +2,7 @@ package com.onarandombox.MultiverseCore.configuration.migration;
import co.aikar.commands.ACFUtil;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import org.bukkit.util.NumberConversions;
import org.bukkit.configuration.file.FileConfiguration;
/**
* Single migrator action that converts a string value to an integer.
@ -21,8 +20,8 @@ public class IntegerMigratorAction implements MigratorAction {
}
@Override
public void migrate(ConfigHandle settings) {
settings.getConfig().set(path, ACFUtil.parseInt(settings.getConfig().getString(path)));
Logging.info("Converted %s to integer %s", path, settings.getConfig().getInt(path));
public void migrate(FileConfiguration config) {
config.set(path, ACFUtil.parseInt(config.getString(path)));
Logging.info("Converted %s to integer %s", path, config.getInt(path));
}
}

View File

@ -1,7 +1,7 @@
package com.onarandombox.MultiverseCore.configuration.migration;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import org.bukkit.configuration.file.FileConfiguration;
/**
* Single migrator action that inverts a boolean value for a given path.
@ -28,9 +28,9 @@ public class InvertBoolMigratorAction implements MigratorAction {
* {@inheritDoc}
*/
@Override
public void migrate(ConfigHandle settings) {
boolean boolValue = !settings.getConfig().getBoolean(path);
settings.getConfig().set(path, boolValue);
public void migrate(FileConfiguration config) {
boolean boolValue = !config.getBoolean(path);
config.set(path, boolValue);
Logging.info("Inverted %s to boolean %s", path, boolValue);
}
}

View File

@ -1,6 +1,6 @@
package com.onarandombox.MultiverseCore.configuration.migration;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import org.bukkit.configuration.file.FileConfiguration;
/**
* A migrator action is a single action that is performed when migrating a config.
@ -10,7 +10,7 @@ public interface MigratorAction {
/**
* Performs the migration action.
*
* @param settings The target settings instance to migrate.
* @param config The target settings instance to migrate.
*/
void migrate(ConfigHandle settings);
void migrate(FileConfiguration config);
}

View File

@ -3,7 +3,7 @@ package com.onarandombox.MultiverseCore.configuration.migration;
import java.util.Optional;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import org.bukkit.configuration.file.FileConfiguration;
/**
* Single migrator action that moves a value from one path to another.
@ -33,11 +33,11 @@ public class MoveMigratorAction implements MigratorAction {
* {@inheritDoc}
*/
@Override
public void migrate(ConfigHandle settings) {
Optional.ofNullable(settings.getConfig().get(fromPath))
public void migrate(FileConfiguration config) {
Optional.ofNullable(config.get(fromPath))
.ifPresent(value -> {
settings.getConfig().set(toPath, value);
settings.getConfig().set(fromPath, null);
config.set(toPath, value);
config.set(fromPath, null);
Logging.config("Moved path %s to %s", fromPath, toPath);
});
}

View File

@ -3,7 +3,7 @@ package com.onarandombox.MultiverseCore.configuration.migration;
import java.util.ArrayList;
import java.util.List;
import com.onarandombox.MultiverseCore.configuration.ConfigHandle;
import org.bukkit.configuration.file.FileConfiguration;
/**
* A version migrator is a collection of migrator actions that are performed when migrating a config to a specific version.
@ -31,10 +31,10 @@ public class VersionMigrator {
/**
* Performs all the migrator actions.
*
* @param settings The target settings instance to migrate.
* @param config The target settings instance to migrate.
*/
public void migrate(ConfigHandle settings) {
actions.forEach(action -> action.migrate(settings));
public void migrate(FileConfiguration config) {
actions.forEach(action -> action.migrate(config));
}
/**

View File

@ -0,0 +1,13 @@
package com.onarandombox.MultiverseCore.configuration.node;
import org.jetbrains.annotations.NotNull;
public interface CommentedNode extends Node {
/**
* Gets the comment of the node.
*
* @return The comment of the node.
*/
@NotNull String[] getComments();
}

View File

@ -0,0 +1,81 @@
package com.onarandombox.MultiverseCore.configuration.node;
import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Strings;
import org.jetbrains.annotations.NotNull;
/**
* A node that represents a header without any value.
*/
public class ConfigHeaderNode implements CommentedNode {
/**
* Creates a new builder for a {@link ConfigHeaderNode}.
*
* @param path The path of the node.
* @return The new builder.
*/
public static @NotNull Builder<? extends Builder<?>> builder(String path) {
return new Builder<>(path);
}
private final @NotNull String path;
private final @NotNull String[] comments;
protected ConfigHeaderNode(@NotNull String path, @NotNull String[] comments) {
this.path = path;
this.comments = comments;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String getPath() {
return path;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String[] getComments() {
return comments;
}
public static class Builder<B extends Builder<B>> {
protected final @NotNull String path;
protected final @NotNull List<String> comments;
public Builder(@NotNull String path) {
this.path = path;
this.comments = new ArrayList<>();
}
/**
* Adds a comment line to the node.
*
* @param comment The comment to add.
* @return This builder.
*/
public @NotNull B comment(@NotNull String comment) {
if (!Strings.isNullOrEmpty(comment) && !comment.startsWith("#")) {
comment = "# " + comment;
}
comments.add(comment);
return (B) this;
}
/**
* Builds the node.
*
* @return The built node.
*/
public @NotNull ConfigHeaderNode build() {
return new ConfigHeaderNode(path, comments.toArray(new String[0]));
}
}
}

View File

@ -1,45 +1,64 @@
package com.onarandombox.MultiverseCore.configuration.node;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of {@link EnhancedValueNode}.
* @param <T> The type of the value.
* A node that contains a value.
* @param <T> The type of the value.
*/
public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode<T> {
public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
/**
* Creates a new builder for a {@link MVValueNode}.
* Creates a new builder for a {@link ConfigNode}.
*
* @param path The path of the node.
* @param type The type of the value.
* @return The new builder.
* @param <T> The type of the value.
*/
public static <T> Builder<T, ? extends Builder> builder(String path, Class<T> type) {
return new Builder<>(path, type);
public static @NotNull <T> ConfigNode.Builder<T, ? extends ConfigNode.Builder<T, ?>> builder(
@NotNull String path,
@NotNull Class<T> type
) {
return new ConfigNode.Builder<>(path, type);
}
protected final Class<T> type;
protected final T defaultValue;
protected final String name;
protected final Function<T, Boolean> validator;
protected final BiConsumer<T, T> onSetValue;
protected final @Nullable String name;
protected final @NotNull Class<T> type;
protected final @Nullable T defaultValue;
protected final @Nullable Function<T, Boolean> validator;
protected final @Nullable BiConsumer<T, T> onSetValue;
protected MVValueNode(String path, String[] comments, Class<T> type, T defaultValue, String name, Function<T, Boolean> validator, BiConsumer<T, T> onSetValue) {
protected ConfigNode(
@NotNull String path,
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<T> type,
@Nullable T defaultValue,
@Nullable Function<T, Boolean> validator,
@Nullable BiConsumer<T, T> onSetValue
) {
super(path, comments);
this.name = name;
this.type = type;
this.defaultValue = defaultValue;
this.name = name;
this.validator = validator;
this.onSetValue = onSetValue;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Option<String> getName() {
return Option.of(name);
}
/**
* {@inheritDoc}
*/
@ -60,15 +79,7 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
* {@inheritDoc}
*/
@Override
public Optional<String> getName() {
return Optional.ofNullable(name);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isValid(T value) {
public boolean validate(@Nullable T value) {
if (validator != null) {
return validator.apply(value);
}
@ -79,25 +90,25 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
* {@inheritDoc}
*/
@Override
public void onSetValue(T oldValue, T newValue) {
public void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
if (onSetValue != null) {
onSetValue.accept(oldValue, newValue);
}
}
/**
* Builder for {@link MVValueNode}.
* Builder for {@link ConfigNode}.
*
* @param <T> The type of the value.
* @param <B> The type of the builder.
*/
public static class Builder<T, B extends Builder<T, B>> extends MVCommentedNode.Builder<B> {
public static class Builder<T, B extends ConfigNode.Builder<T, B>> extends ConfigHeaderNode.Builder<B> {
protected final Class<T> type;
protected T defaultValue;
protected String name;
protected Function<T, Boolean> validator;
protected BiConsumer<T, T> onSetValue;
protected @Nullable String name;
protected @NotNull final Class<T> type;
protected @Nullable T defaultValue;
protected @Nullable Function<T, Boolean> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
/**
* Creates a new builder.
@ -107,8 +118,8 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
*/
protected Builder(@NotNull String path, @NotNull Class<T> type) {
super(path);
this.type = type;
this.name = path;
this.type = type;
}
/**
@ -117,7 +128,7 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
* @param defaultValue The default value.
* @return This builder.
*/
public B defaultValue(@NotNull T defaultValue) {
public @NotNull B defaultValue(@NotNull T defaultValue) {
this.defaultValue = defaultValue;
return (B) this;
}
@ -128,12 +139,12 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
* @param name The name of this node.
* @return This builder.
*/
public B name(@Nullable String name) {
public @NotNull B name(@Nullable String name) {
this.name = name;
return (B) this;
}
public B validator(@Nullable Function<T, Boolean> validator) {
public @NotNull B validator(@NotNull Function<T, Boolean> validator) {
this.validator = validator;
return (B) this;
}
@ -144,7 +155,7 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
* @param onSetValue The action to be performed.
* @return This builder.
*/
public B onSetValue(@Nullable BiConsumer<T, T> onSetValue) {
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
this.onSetValue = onSetValue;
return (B) this;
}
@ -153,8 +164,8 @@ public class MVValueNode<T> extends MVCommentedNode implements EnhancedValueNode
* {@inheritDoc}
*/
@Override
public MVValueNode<T> build() {
return new MVValueNode<>(path, comments.toArray(new String[0]), type, defaultValue, name, validator, onSetValue);
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]), name, type, defaultValue, validator, onSetValue);
}
}
}

View File

@ -0,0 +1,14 @@
package com.onarandombox.MultiverseCore.configuration.node;
import com.onarandombox.MultiverseCore.exceptions.MultiverseException;
import org.jetbrains.annotations.Nullable;
import static com.onarandombox.MultiverseCore.utils.MVCorei18n.CONFIG_NODE_NOTFOUND;
import static com.onarandombox.MultiverseCore.utils.message.MessageReplacement.replace;
public class ConfigNodeNotFoundException extends MultiverseException {
public ConfigNodeNotFoundException(@Nullable String nodeName) {
super(CONFIG_NODE_NOTFOUND.bundle("Config node not found: {node}", replace("{node}").with(nodeName)), null);
}
}

View File

@ -1,29 +0,0 @@
package com.onarandombox.MultiverseCore.configuration.node;
import java.util.Optional;
import io.github.townyadvanced.commentedconfiguration.setting.TypedValueNode;
/**
* A {@link TypedValueNode} that has a name, validation, and action to be performed when the value is set.
*
* @param <T> The type of the node's value.
*/
public interface EnhancedValueNode<T> extends TypedValueNode<T> {
/**
* Gets the name of this node. Used for identifying the node from user input.
*
* @return The name of this node.
*/
Optional<String> getName();
boolean isValid(T value);
/**
* Called when the value of this node is set.
*
* @param oldValue The old value.
* @param newValue The new value.
*/
void onSetValue(T oldValue, T newValue);
}

View File

@ -1,91 +0,0 @@
package com.onarandombox.MultiverseCore.configuration.node;
import java.util.ArrayList;
import java.util.List;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
import org.jetbrains.annotations.NotNull;
/**
* Implementation of {@link CommentedNode} that allows for comments to be added to the node.
*/
public class MVCommentedNode implements CommentedNode {
/**
* Creates a new builder for a {@link MVCommentedNode}.
*
* @param path The path of the node.
* @return The new builder.
*/
public static Builder<Builder> builder(String path) {
return new Builder<>(path);
}
protected final String path;
protected final String[] comments;
protected MVCommentedNode(String path, String[] comments) {
this.path = path;
this.comments = comments;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String getPath() {
return path;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String[] getComments() {
return comments;
}
/**
* Builder for {@link MVCommentedNode}.
*
* @param <B> The type of the builder.
*/
public static class Builder<B extends Builder> {
protected final String path;
protected final List<String> comments;
/**
* Creates a new builder for a {@link MVCommentedNode}.
*
* @param path The path of the node.
*/
protected Builder(String path) {
this.path = path;
this.comments = new ArrayList<>();
}
/**
* Adds a comment line to the node.
*
* @param comment The comment to add.
* @return This builder.
*/
public B comment(@NotNull String comment) {
if (!comment.isEmpty() && !comment.trim().startsWith("#")) {
// Automatically add a comment prefix if the comment doesn't start with one.
comment = "# " + comment;
}
this.comments.add(comment);
return (B) this;
}
/**
* Builds the node.
*
* @return The built node.
*/
public MVCommentedNode build() {
return new MVCommentedNode(path, comments.toArray(new String[0]));
}
}
}

View File

@ -0,0 +1,13 @@
package com.onarandombox.MultiverseCore.configuration.node;
import org.jetbrains.annotations.NotNull;
public interface Node {
/**
* Gets the YAML path of the node.
*
* @return The YAML path of the node.
*/
@NotNull String getPath();
}

View File

@ -5,38 +5,38 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
/**
* A collection of {@link CommentedNode}s, with mappings to nodes by name.
*/
public class NodeGroup implements Collection<CommentedNode> {
private final Collection<CommentedNode> nodes;
private final Map<String, CommentedNode> nodesMap;
public class NodeGroup implements Collection<Node> {
private final Collection<Node> nodes;
private final Map<String, Node> nodesMap;
public NodeGroup() {
this.nodes = new ArrayList<>();
this.nodesMap = new HashMap<>();
}
public NodeGroup(Collection<CommentedNode> nodes) {
public NodeGroup(Collection<Node> nodes) {
this.nodes = nodes;
this.nodesMap = new HashMap<>(nodes.size());
nodes.forEach(this::addNodeIndex);
}
private void addNodeIndex(CommentedNode node) {
if (node instanceof EnhancedValueNode) {
((EnhancedValueNode<?>) node).getName().ifPresent(name -> nodesMap.put(name, node));
private void addNodeIndex(Node node) {
if (node instanceof ValueNode) {
((ValueNode<?>) node).getName().peek(name -> nodesMap.put(name, node));
}
}
private void removeNodeIndex(CommentedNode node) {
if (node instanceof EnhancedValueNode) {
((EnhancedValueNode<?>) node).getName().ifPresent(nodesMap::remove);
private void removeNodeIndex(Node node) {
if (node instanceof ValueNode) {
((ValueNode<?>) node).getName().peek(nodesMap::remove);
}
}
@ -52,11 +52,22 @@ public class NodeGroup implements Collection<CommentedNode> {
/**
* Gets the node with the given name.
*
* @param name The name of the node to get.
* @return The node with the given name, or {@link Optional#empty()} if no node with the given name exists.
* @param name The name of the node to get.
* @return The node with the given name, or {@link Option.None} if no node with the given name exists.
*/
public Optional<CommentedNode> findNode(String name) {
return Optional.ofNullable(nodesMap.get(name));
public Option<Node> findNode(String name) {
return Option.of(nodesMap.get(name));
}
/**
* Gets the node with the given name.
*
* @param name The name of the node to get.
* @param type The type of the node to get.
* @return The node with the given name, or {@link Option.None} if no node with the given name exists.
*/
public <T extends Node> Option<T> findNode(String name, Class<T> type) {
return Option.of(nodesMap.get(name)).map(node -> type.isAssignableFrom(node.getClass()) ? (T) node : null);
}
@Override
@ -76,7 +87,7 @@ public class NodeGroup implements Collection<CommentedNode> {
@NotNull
@Override
public Iterator<CommentedNode> iterator() {
public Iterator<Node> iterator() {
return nodes.iterator();
}
@ -91,9 +102,9 @@ public class NodeGroup implements Collection<CommentedNode> {
}
@Override
public boolean add(CommentedNode commentedNode) {
if (nodes.add(commentedNode)) {
addNodeIndex(commentedNode);
public boolean add(Node node) {
if (nodes.add(node)) {
addNodeIndex(node);
return true;
}
return false;
@ -102,7 +113,7 @@ public class NodeGroup implements Collection<CommentedNode> {
@Override
public boolean remove(Object o) {
if (nodes.remove(o) && o instanceof CommentedNode) {
removeNodeIndex((CommentedNode) o);
removeNodeIndex((Node) o);
return true;
}
return false;
@ -114,7 +125,7 @@ public class NodeGroup implements Collection<CommentedNode> {
}
@Override
public boolean addAll(@NotNull Collection<? extends CommentedNode> collection) {
public boolean addAll(@NotNull Collection<? extends Node> collection) {
return nodes.addAll(collection);
}

View File

@ -0,0 +1,45 @@
package com.onarandombox.MultiverseCore.configuration.node;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface ValueNode<T> extends Node {
/**
* Gets the name of this node. Used for identifying the node from user input.
*
* @return An {@link Option} containing the name of this node, or {@link Option.None} if the node has no name.
*/
@NotNull Option<String> getName();
/**
* Gets the class type {@link T} of the node value.
*
* @return The class type of the node value.
*/
@NotNull Class<T> getType();
/**
* Gets the default value with type {@link T} of the node.
*
* @return The default value of the node.
*/
@Nullable T getDefaultValue();
/**
* Validates the value of this node.
*
* @param value The value to validate.
* @return True if the value is valid, false otherwise.
*/
boolean validate(@Nullable T value);
/**
* Called when the value of this node is set.
*
* @param oldValue The old value.
* @param newValue The new value.
*/
void onSetValue(@Nullable T oldValue, @Nullable T newValue);
}

View File

@ -9,6 +9,7 @@ import org.jetbrains.annotations.NotNull;
public enum MVCorei18n implements MessageKeyProvider {
// config status
CONFIG_SAVE_FAILED,
CONFIG_NODE_NOTFOUND,
// check command
CHECK_CHECKING,

View File

@ -1,5 +1,6 @@
# configuration
mv-core.config.save.failed=Unable to save Multiverse-Core config.yml. Your changes will be temporary!
mv-core.config.node.notfound=Node not found in config: {node}
# /mv check
mv-core.check.description=Checks if a player can teleport to a destination.
@ -113,4 +114,3 @@ mv-core.unload.success=&aUnloaded world '{world}'!
# /mv usage
mv-core.usage.description=Show Multiverse-Core command usage.

View File

@ -59,14 +59,14 @@ class ConfigTest : TestWithMockBukkit() {
@Test
fun `Getting existing config property with getProperty returns expected value`() {
assertEquals(false, config.getProperty("enforce-access"))
assertEquals("world", config.getProperty("first-spawn-location"))
assertEquals(false, config.getProperty("enforce-access").get())
assertEquals("world", config.getProperty("first-spawn-location").get())
}
@Test
fun `Getting non-existing config property with getProperty returns null`() {
assertNull(config.getProperty("invalid-property"))
assertNull(config.getProperty("version"))
assertTrue(config.getProperty("invalid-property").isFailure)
assertTrue(config.getProperty("version").isFailure)
}
@Test
@ -77,19 +77,19 @@ class ConfigTest : TestWithMockBukkit() {
@Test
fun `Updating an existing config property with setProperty reflects the changes in getProperty`() {
assertTrue(config.setProperty("enforce-access", true))
assertEquals(true, config.getProperty("enforce-access"))
assertTrue(config.setProperty("enforce-access", true).isSuccess)
assertEquals(true, config.getProperty("enforce-access").get())
assertTrue(config.setProperty("first-spawn-location", "world2"))
assertEquals("world2", config.getProperty("first-spawn-location"))
assertTrue(config.setProperty("first-spawn-location", "world2").isSuccess)
assertEquals("world2", config.getProperty("first-spawn-location").get())
assertTrue(config.setProperty("global-debug", 1))
assertEquals(1, config.getProperty("global-debug"))
assertTrue(config.setProperty("global-debug", 1).isSuccess)
assertEquals(1, config.getProperty("global-debug").get())
}
@Test
fun `Updating a non-existing property with setProperty returns false`() {
assertFalse(config.setProperty("invalid-property", false))
assertFalse(config.setProperty("version", 1.1))
assertTrue(config.setProperty("invalid-property", false).isFailure)
assertTrue(config.setProperty("version", 1.1).isFailure)
}
}