From 2f834ebfd66e92e8db523a8c221abc7228eb0af5 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 27 Mar 2023 18:30:38 +0800 Subject: [PATCH 1/9] fix: Relocation of acf dependency --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4b1e77c1..03f132ca 100644 --- a/build.gradle +++ b/build.gradle @@ -241,7 +241,7 @@ javadoc { project.configurations.api.canBeResolved = true shadowJar { - relocate 'co.aikar', 'com.onarandombox.acf' + relocate 'co.aikar.commands', 'com.onarandombox.acf' relocate 'com.dumptruckman.minecraft.util.Logging', 'com.onarandombox.MultiverseCore.utils.CoreLogging' relocate 'com.dumptruckman.minecraft.util.DebugLog', 'com.onarandombox.MultiverseCore.utils.DebugFileLogger' relocate 'de.themoep.idconverter', 'com.onarandombox.idconverter' From 40d541762974734a17702e59f34e24deadabf8db Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 00:51:54 -0400 Subject: [PATCH 2/9] Make PluginLocales injectable. --- .../MultiverseCore/MultiverseCore.java | 50 +++++++++++++++---- .../commandtools/MVCommandManager.java | 23 +++------ .../commandtools/PluginLocales.java | 8 ++- .../multiverse/core/inject/InjectionTest.kt | 6 +++ 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java b/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java index cda4e7e9..31e2cfee 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java +++ b/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java @@ -22,6 +22,7 @@ import com.onarandombox.MultiverseCore.api.MVWorld; import com.onarandombox.MultiverseCore.api.MVWorldManager; import com.onarandombox.MultiverseCore.commandtools.MVCommandManager; import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; import com.onarandombox.MultiverseCore.config.MVCoreConfigProvider; import com.onarandombox.MultiverseCore.destination.DestinationsProvider; import com.onarandombox.MultiverseCore.economy.MVEconomist; @@ -67,6 +68,8 @@ public class MultiverseCore extends JavaPlugin implements MVCore { private Provider metricsConfiguratorProvider; @Inject private Provider economistProvider; + @Inject + private Provider pluginLocalesProvider; // Counter for the number of plugins that have registered with us private int pluginCount; @@ -173,13 +176,19 @@ public class MultiverseCore extends JavaPlugin implements MVCore { private void loadEconomist() { Try.run(() -> economistProvider.get()) - .onFailure(e -> Logging.severe("Failed to load economy integration", e)); + .onFailure(e -> { + Logging.severe("Failed to load economy integration"); + e.printStackTrace(); + }); } private void loadAnchors() { Try.of(() -> anchorManagerProvider.get()) .onSuccess(AnchorManager::loadAnchors) - .onFailure(e -> Logging.severe("Failed to load anchors", e)); + .onFailure(e -> { + Logging.severe("Failed to load anchors"); + e.printStackTrace(); + }); } /** @@ -204,7 +213,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { serviceLocator.getAllServices(MultiverseCommand.class) .forEach(commandManager::registerCommand); }) - .onFailure(e -> Logging.severe("Failed to register commands", e)); + .onFailure(e -> { + Logging.severe("Failed to register commands"); + e.printStackTrace(); + }); } /** @@ -212,12 +224,18 @@ public class MultiverseCore extends JavaPlugin implements MVCore { */ private void setUpLocales() { Try.of(() -> commandManagerProvider.get()) - .andThenTry(commandManager -> { + .andThen(commandManager -> { commandManager.usePerIssuerLocale(true, true); - commandManager.getLocales().addFileResClassLoader(this); - commandManager.getLocales().addMessageBundles("multiverse-core"); }) - .onFailure(e -> Logging.severe("Failed to register locales", e)); + .mapTry(commandManager -> pluginLocalesProvider.get()) + .andThen(pluginLocales -> { + pluginLocales.addFileResClassLoader(this); + pluginLocales.addMessageBundles("multiverse-core"); + }) + .onFailure(e -> { + Logging.severe("Failed to register locales"); + e.printStackTrace(); + }); } /** @@ -229,7 +247,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { serviceLocator.getAllServices(Destination.class) .forEach(destinationsProvider::registerDestination); }) - .onFailure(e -> Logging.severe("Failed to register destinations", e)); + .onFailure(e -> { + Logging.severe("Failed to register destinations"); + e.printStackTrace(); + }); } /** @@ -239,7 +260,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { if (TestingMode.isDisabled()) { // Load metrics Try.of(() -> metricsConfiguratorProvider.get()) - .onFailure(e -> Logging.severe("Failed to setup metrics", e)); + .onFailure(e -> { + Logging.severe("Failed to setup metrics"); + e.printStackTrace(); + }); } else { Logging.info("Metrics are disabled in testing mode."); } @@ -260,7 +284,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { private void loadPlaceholderAPIIntegration() { if(getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) { Try.run(() -> serviceLocator.createAndInitialize(MultiverseCorePlaceholders.class)) - .onFailure(e -> Logging.severe("Failed to load PlaceholderAPI integration.", e)); + .onFailure(e -> { + Logging.severe("Failed to load PlaceholderAPI integration."); + e.printStackTrace(); + }); } } @@ -348,7 +375,8 @@ public class MultiverseCore extends JavaPlugin implements MVCore { return getConfigProvider().saveConfig() .map(v -> true) .recover(e -> { - Logging.severe(e.getMessage(), e); + Logging.severe(e.getMessage()); + e.printStackTrace(); return false; }) .get(); diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java index b64236ce..0b46b282 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java @@ -28,7 +28,6 @@ public class MVCommandManager extends PaperCommandManager { private final CommandQueueManager commandQueueManager; private final Provider commandContextsProvider; private final Provider commandCompletionsProvider; - private PluginLocales pluginLocales; @Inject public MVCommandManager( @@ -48,6 +47,13 @@ public class MVCommandManager extends PaperCommandManager { MVCommandConditions.load(this, worldManager); } + void loadLanguages(PluginLocales locales) { + if (this.locales == null) { + this.locales = locales; + this.locales.loadLanguages(); + } + } + /** * Gets class responsible for flag handling. * @@ -57,21 +63,6 @@ public class MVCommandManager extends PaperCommandManager { return flagsManager; } - /** - * Gets class responsible for locale handling. - * - * @return A not-null {@link PluginLocales}. - */ - @Override - public PluginLocales getLocales() { - if (this.pluginLocales == null) { - this.pluginLocales = new PluginLocales(this); - this.locales = pluginLocales; // For parent class - this.pluginLocales.loadLanguages(); - } - return this.pluginLocales; - } - /** * Manager for command that requires /mv confirm before execution. * diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java index 66c0f8c1..2db5686f 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java @@ -1,14 +1,16 @@ package com.onarandombox.MultiverseCore.commandtools; -import co.aikar.commands.BukkitCommandManager; import co.aikar.commands.BukkitLocales; import com.onarandombox.MultiverseCore.utils.file.FileResClassLoader; +import jakarta.inject.Inject; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; /** * Locale manager with additional methods for loading locales from plugin's locales folder. */ +@Service public class PluginLocales extends BukkitLocales { private static final String DEFAULT_LOCALE_FOLDER_PATH = "locales"; @@ -18,8 +20,10 @@ public class PluginLocales extends BukkitLocales { * * @param manager The command manager. */ - public PluginLocales(BukkitCommandManager manager) { + @Inject + public PluginLocales(MVCommandManager manager) { super(manager); + manager.loadLanguages(this); } /** diff --git a/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt index 6b9a8d79..95b657b3 100644 --- a/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt @@ -9,6 +9,7 @@ import com.onarandombox.MultiverseCore.api.MVWorldManager import com.onarandombox.MultiverseCore.api.SafeTTeleporter import com.onarandombox.MultiverseCore.commandtools.MVCommandManager import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand +import com.onarandombox.MultiverseCore.commandtools.PluginLocales import com.onarandombox.MultiverseCore.config.MVCoreConfigProvider import com.onarandombox.MultiverseCore.economy.MVEconomist import com.onarandombox.MultiverseCore.listeners.MVChatListener @@ -142,4 +143,9 @@ class InjectionTest : TestWithMockBukkit() { // Also making sure this is not loaded automatically since it's supposed to be disabled during tests assertNull(multiverseCore.getService(MetricsConfigurator::class.java)) } + + @Test + fun `PluginLocales is available as a service`() { + assertNotNull(multiverseCore.getService(PluginLocales::class.java)) + } } From 224435f6cd07f4b822aaac230e2321a999a432b0 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 12:53:40 -0400 Subject: [PATCH 3/9] Add Message for bundling messages with their arguments. --- .../commandtools/MVCommandManager.java | 6 + .../utils/message/LocalizedMessage.java | 40 +++++ .../MultiverseCore/utils/message/Message.java | 140 +++++++++++++++ .../utils/message/MessageReplacement.java | 68 ++++++++ .../core/commandtools/LocalizationTest.kt | 160 ++++++++++++++++++ 5 files changed, 414 insertions(+) create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java create mode 100644 src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java index 0b46b282..bb8f43cc 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java @@ -7,6 +7,7 @@ import co.aikar.commands.BukkitCommandExecutionContext; import co.aikar.commands.CommandCompletions; import co.aikar.commands.CommandContexts; import co.aikar.commands.CommandHelp; +import co.aikar.commands.CommandIssuer; import co.aikar.commands.HelpEntry; import co.aikar.commands.PaperCommandManager; import com.onarandombox.MultiverseCore.MultiverseCore; @@ -15,6 +16,7 @@ import com.onarandombox.MultiverseCore.commandtools.flags.CommandFlagsManager; import com.onarandombox.MultiverseCore.commandtools.queue.CommandQueueManager; import jakarta.inject.Inject; import jakarta.inject.Provider; +import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; @@ -111,4 +113,8 @@ public class MVCommandManager extends PaperCommandManager { } help.showHelp(); } + + public @NotNull CommandIssuer getConsoleCommandIssuer() { + return getCommandIssuer(Bukkit.getConsoleSender()); + } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java new file mode 100644 index 00000000..fcf77f7a --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java @@ -0,0 +1,40 @@ +package com.onarandombox.MultiverseCore.utils.message; + +import co.aikar.commands.ACFUtil; +import co.aikar.commands.CommandIssuer; +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +final class LocalizedMessage extends Message implements MessageKeyProvider { + + private final @NotNull MessageKeyProvider messageKeyProvider; + + LocalizedMessage( + @NotNull MessageKeyProvider messageKeyProvider, + @NotNull String message, + @NotNull MessageReplacement... replacements + ) { + super(message, replacements); + this.messageKeyProvider = messageKeyProvider; + } + + @Override + public MessageKey getMessageKey() { + return messageKeyProvider.getMessageKey(); + } + + @Override + public @NotNull String formatted(@NotNull PluginLocales locales, @Nullable CommandIssuer commandIssuer) { + Objects.requireNonNull(locales, "locales must not be null"); + + if (getReplacements().length == 0) { + return raw(); + } + return ACFUtil.replaceStrings(locales.getMessage(commandIssuer, getMessageKey()), getReplacements()); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java new file mode 100644 index 00000000..76821066 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java @@ -0,0 +1,140 @@ +package com.onarandombox.MultiverseCore.utils.message; + +import co.aikar.commands.ACFUtil; +import co.aikar.commands.CommandIssuer; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * A message that can be formatted with replacements and localized. + */ +public sealed class Message permits LocalizedMessage { + + /** + * Creates a basic non-localized Message with the given message and replacements. + * + * @param message The message + * @param replacements The replacements + * @return A new Message + */ + @Contract(value = "_, _ -> new", pure = true) + public static Message of(@NotNull String message, @NotNull MessageReplacement... replacements) { + Objects.requireNonNull(message, "message must not be null"); + for (MessageReplacement replacement : replacements) { + Objects.requireNonNull(replacement, "replacements must not contain null"); + } + + return new Message(message, replacements); + } + + /** + * Creates a localized Message with the given message key provider, non-localized message and replacements. + *
+ * The non-localized message is required for conditions where it is not practical to provide a localized message. + *
+ * This message will extend {@link MessageKeyProvider} and delegate to the given message key provider. + * + * @param messageKeyProvider The message key provider + * @param nonLocalizedMessage The non-localized message + * @param replacements The replacements + * @return A new localizable Message + */ + @Contract(value = "_, _, _ -> new", pure = true) + public static Message of( + @NotNull MessageKeyProvider messageKeyProvider, + @NotNull String nonLocalizedMessage, + @NotNull MessageReplacement... replacements + ) { + Objects.requireNonNull(messageKeyProvider, "messageKeyProvider must not be null"); + Objects.requireNonNull(nonLocalizedMessage, "message must not be null"); + for (MessageReplacement replacement : replacements) { + Objects.requireNonNull(replacement, "replacements must not contain null"); + } + + return new LocalizedMessage(messageKeyProvider, nonLocalizedMessage, replacements); + } + + private final @NotNull String message; + private final @NotNull String[] replacements; + + protected Message(@NotNull String message, @NotNull MessageReplacement... replacements) { + this.message = message; + this.replacements = toReplacementsArray(replacements); + } + + /** + * Gets the replacements for this message. + *
+ * This array is guaranteed to be of even length and suitable for use with + * {@link ACFUtil#replaceStrings(String, String...)}. + * + * @return The replacements + */ + public @NotNull String[] getReplacements() { + return replacements; + } + + /** + * Gets the raw, non-localized, non-replaced message. + * + * @return The raw message + */ + public @NotNull String raw() { + return message; + } + + /** + * Gets the formatted message. + *
+ * This is the raw, non-localized message with replacements applied. + * + * @return The formatted message + */ + public @NotNull String formatted() { + if (replacements.length == 0) { + return raw(); + } + return ACFUtil.replaceStrings(message, replacements); + } + + /** + * Gets the formatted message from localization data. + *
+ * This is the localized message with replacements applied. The message is localized using the default locale. + * + * @param locales The MultiverseCore locales provider + * @return The formatted, localized message + */ + public @NotNull String formatted(@NotNull PluginLocales locales) { + return formatted(locales, null); + } + + /** + * Gets the formatted message from localization data. + *
+ * This is the localized message with replacements applied. The message is localized using the locale of the given + * command issuer, if not null. + * + * @param locales The MultiverseCore locales provider + * @param commandIssuer The command issuer the message is for, or null for the console (default locale) + * @return The formatted, localized message + */ + public @NotNull String formatted(@NotNull PluginLocales locales, @Nullable CommandIssuer commandIssuer) { + return formatted(); + } + + private static String[] toReplacementsArray(@NotNull MessageReplacement... replacements) { + String[] replacementsArray = new String[replacements.length * 2]; + int i = 0; + for (MessageReplacement replacement : replacements) { + replacementsArray[i++] = replacement.getKey(); + replacementsArray[i++] = replacement.getReplacement(); + } + return replacementsArray; + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java new file mode 100644 index 00000000..e6301a35 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java @@ -0,0 +1,68 @@ +package com.onarandombox.MultiverseCore.utils.message; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Captures string replacements for {@link Message}s. + */ +public final class MessageReplacement { + + /** + * Creates a replacement key for the given key string. + * + * @param key The string to replace + * @return A new replacement key which can be used to create a replacement + */ + @Contract(value = "_ -> new", pure = true) + public static MessageReplacement.Key replace(@NotNull String key) { + return new MessageReplacement.Key(key); + } + + public static final class Key { + + private final @NotNull String key; + + private Key(@NotNull String key) { + this.key = key; + } + + /** + * Creates a replacement for this key. + * + * @param replacement The replacement value, if null it will be replaced with a string equal to "null" + * @return A new message replacement + */ + @Contract(value = "_ -> new", pure = true) + public MessageReplacement with(@Nullable Object replacement) { + return new MessageReplacement(key, replacement); + } + } + + private final @NotNull String key; + private final @NotNull String replacement; + + private MessageReplacement(@NotNull String key, @Nullable Object replacement) { + this.key = key; + this.replacement = String.valueOf(replacement); + } + + /** + * Gets the string to be replaced. + * + * @return The key + */ + public @NotNull String getKey() { + return key; + } + + /** + * Gets the replacement value. + * + * @return The replacement + */ + public @NotNull String getReplacement() { + return replacement; + } +} diff --git a/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt b/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt new file mode 100644 index 00000000..fbfd9f23 --- /dev/null +++ b/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt @@ -0,0 +1,160 @@ +package org.mvplugins.multiverse.core.commandtools + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.containsSubstring +import com.onarandombox.MultiverseCore.commandtools.MVCommandManager +import com.onarandombox.MultiverseCore.commandtools.PluginLocales +import com.onarandombox.MultiverseCore.utils.MVCorei18n +import com.onarandombox.MultiverseCore.utils.message.Message +import com.onarandombox.MultiverseCore.utils.message.MessageReplacement.replace +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.mvplugins.multiverse.core.TestWithMockBukkit +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull + +class LocalizationTest : TestWithMockBukkit() { + + private lateinit var locales: PluginLocales + private lateinit var commandManager: MVCommandManager + + @BeforeTest + fun setUpLocale() { + locales = assertNotNull(multiverseCore.getService(PluginLocales::class.java)) + commandManager = assertNotNull(multiverseCore.getService(MVCommandManager::class.java)) + } + + @Nested + @DisplayName("Given a Message with only a non-localized message") + inner class BasicMessage { + + private val messageString = "This is a test message" + private val message = Message.of(messageString) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the same as the original`() { + assertEquals(messageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be the same as the original`() { + assertEquals(messageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales for a CommandIssuer should be the same as the original`() { + assertEquals(messageString, message.formatted(locales, commandManager.consoleCommandIssuer)) + } + } + + @Nested + @DisplayName("Given a Message with a non-localized message and one replacement") + inner class MessageWithOneReplacement { + + private val replacementKey = "{count}" + private val messageString = "This is a test message with $replacementKey replacement" + private val replacedMessageString = messageString.replace(replacementKey, "one") + + private val message = Message.of(messageString, replace(replacementKey).with("one")) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales for a CommandIssuer should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales, commandManager.consoleCommandIssuer)) + } + } + + @Nested + @DisplayName("Given a Message with a non-localized message and two replacements") + inner class MessageWithTwoReplacements { + + private val replacementKey1 = "{thing1}" + private val replacementKey2 = "{thing2}" + private val messageString = "$replacementKey1 $replacementKey2" + private val replacedMessageString = messageString + .replace(replacementKey1, "one") + .replace(replacementKey2, "two") + + private val message = Message.of( + messageString, + replace(replacementKey1).with("one"), + replace(replacementKey2).with("two"), + ) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales for a CommandIssuer should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales, commandManager.consoleCommandIssuer)) + } + } + + @Nested + @DisplayName("Given a Message with a localized message with one replacement") + inner class LocalizedMessage { + + private val replacementKey = "{world}" + private val replacementValue = "World" + private val messageString = "Hello $replacementKey!" + private val replacedMessageString = messageString.replace(replacementKey, replacementValue) + + private val message = MVCorei18n.CLONE_SUCCESS + .bundle(messageString, replace(replacementKey).with(replacementValue)) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the replaced original string`() { + assertEquals(replacedMessageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be different from the replaced original string`() { + assertNotEquals(replacedMessageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales should have performed replacement`() { + assertThat(message.formatted(locales), !containsSubstring(replacementKey)) + assertThat(message.formatted(locales), containsSubstring(replacementValue)) + } + } +} From 979e80e1bdc1f70176ee9a1861c73c473a997d5e Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 13:09:18 -0400 Subject: [PATCH 4/9] Use a custom implementation of BukkitCommandIssuer. --- .../commands/OpenBukkitCommandIssuer.java | 13 ++++++++++++ .../commandtools/MVCommandIssuer.java | 20 +++++++++++++++++++ .../commandtools/MVCommandManager.java | 10 ++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java diff --git a/src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java b/src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java new file mode 100644 index 00000000..3d136f3a --- /dev/null +++ b/src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java @@ -0,0 +1,13 @@ +package co.aikar.commands; + +import org.bukkit.command.CommandSender; + +/** + * Exists just so we can extend BukkitCommandIssuer since it has a package-private constructor. + */ +public abstract class OpenBukkitCommandIssuer extends BukkitCommandIssuer { + + protected OpenBukkitCommandIssuer(BukkitCommandManager manager, CommandSender sender) { + super(manager, sender); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java new file mode 100644 index 00000000..3a954c83 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java @@ -0,0 +1,20 @@ +package com.onarandombox.MultiverseCore.commandtools; + +import co.aikar.commands.OpenBukkitCommandIssuer; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class MVCommandIssuer extends OpenBukkitCommandIssuer { + + private final MVCommandManager commandManager; + + MVCommandIssuer(@NotNull MVCommandManager commandManager, @NotNull CommandSender sender) { + super(commandManager, sender); + this.commandManager = commandManager; + } + + @Override + public MVCommandManager getManager() { + return commandManager; + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java index bb8f43cc..d1e760fa 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java @@ -17,6 +17,7 @@ import com.onarandombox.MultiverseCore.commandtools.queue.CommandQueueManager; import jakarta.inject.Inject; import jakarta.inject.Provider; import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; @@ -117,4 +118,13 @@ public class MVCommandManager extends PaperCommandManager { public @NotNull CommandIssuer getConsoleCommandIssuer() { return getCommandIssuer(Bukkit.getConsoleSender()); } + + @Override + public @NotNull MVCommandIssuer getCommandIssuer(Object issuer) { + if (!(issuer instanceof CommandSender)) { + throw new IllegalArgumentException(issuer.getClass().getName() + " is not a Command Issuer."); + } else { + return new MVCommandIssuer(this, (CommandSender)issuer); + } + } } From 1f8f68c38357ab39b8e90bc763b9b97689ab5cf8 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 15:18:08 -0400 Subject: [PATCH 5/9] Add kotlin mockito dependency for tests. --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 03f132ca..bfef55d9 100644 --- a/build.gradle +++ b/build.gradle @@ -114,6 +114,7 @@ dependencies { } testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'com.natpryce:hamkrest:1.8.0.1' + testImplementation 'org.mockito.kotlin:mockito-kotlin:4.1.0' // Old Tests oldTestImplementation 'org.spigotmc:spigot-api:1.19.3-R0.1-SNAPSHOT' From 032a8c366dfbe7045f8614a267a57643aa57e431 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 23:45:44 -0400 Subject: [PATCH 6/9] Add send message methods to MVCommandIssuer. --- .../commandtools/MVCommandIssuer.java | 28 ++++++ .../commandtools/MVCommandManager.java | 9 +- .../core/commandtools/LocalizationTest.kt | 96 +++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java index 3a954c83..f45b3a71 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java @@ -1,6 +1,9 @@ package com.onarandombox.MultiverseCore.commandtools; +import co.aikar.commands.MessageType; import co.aikar.commands.OpenBukkitCommandIssuer; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.message.Message; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; @@ -17,4 +20,29 @@ public class MVCommandIssuer extends OpenBukkitCommandIssuer { public MVCommandManager getManager() { return commandManager; } + + public void sendError(Message message) { + sendMessage(MessageType.ERROR, message); + } + + public void sendSyntax(Message message) { + sendMessage(MessageType.SYNTAX, message); + } + + public void sendInfo(Message message) { + sendMessage(MessageType.INFO, message); + } + + private void sendMessage(MessageType messageType, Message message) { + if (message instanceof MessageKeyProvider) { + sendMessage(messageType, (MessageKeyProvider) message, message.getReplacements()); + } else { + var formatter = getManager().getFormat(messageType); + if (formatter != null) { + sendMessage(formatter.format(message.formatted())); + } else { + sendMessage(message.formatted()); + } + } + } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java index d1e760fa..b3880933 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java @@ -4,10 +4,10 @@ import java.util.List; import co.aikar.commands.BukkitCommandCompletionContext; import co.aikar.commands.BukkitCommandExecutionContext; +import co.aikar.commands.BukkitLocales; import co.aikar.commands.CommandCompletions; import co.aikar.commands.CommandContexts; import co.aikar.commands.CommandHelp; -import co.aikar.commands.CommandIssuer; import co.aikar.commands.HelpEntry; import co.aikar.commands.PaperCommandManager; import com.onarandombox.MultiverseCore.MultiverseCore; @@ -57,6 +57,11 @@ public class MVCommandManager extends PaperCommandManager { } } + @Override + public BukkitLocales getLocales() { + return this.locales; + } + /** * Gets class responsible for flag handling. * @@ -115,7 +120,7 @@ public class MVCommandManager extends PaperCommandManager { help.showHelp(); } - public @NotNull CommandIssuer getConsoleCommandIssuer() { + public @NotNull MVCommandIssuer getConsoleCommandIssuer() { return getCommandIssuer(Bukkit.getConsoleSender()); } diff --git a/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt b/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt index fbfd9f23..bfd223eb 100644 --- a/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt @@ -2,13 +2,19 @@ package org.mvplugins.multiverse.core.commandtools import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.containsSubstring +import com.onarandombox.MultiverseCore.commandtools.MVCommandIssuer import com.onarandombox.MultiverseCore.commandtools.MVCommandManager import com.onarandombox.MultiverseCore.commandtools.PluginLocales import com.onarandombox.MultiverseCore.utils.MVCorei18n import com.onarandombox.MultiverseCore.utils.message.Message import com.onarandombox.MultiverseCore.utils.message.MessageReplacement.replace +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify import org.mvplugins.multiverse.core.TestWithMockBukkit import kotlin.test.BeforeTest import kotlin.test.Test @@ -53,6 +59,27 @@ class LocalizationTest : TestWithMockBukkit() { fun `The formatted message with PluginLocales for a CommandIssuer should be the same as the original`() { assertEquals(messageString, message.formatted(locales, commandManager.consoleCommandIssuer)) } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + verify(sender).sendMessage("§9§9$messageString") + } + } } @Nested @@ -84,6 +111,27 @@ class LocalizationTest : TestWithMockBukkit() { fun `The formatted message with PluginLocales for a CommandIssuer should be the replaced message string`() { assertEquals(replacedMessageString, message.formatted(locales, commandManager.consoleCommandIssuer)) } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + verify(sender).sendMessage("§9§9$replacedMessageString") + } + } } @Nested @@ -122,6 +170,27 @@ class LocalizationTest : TestWithMockBukkit() { fun `The formatted message with PluginLocales for a CommandIssuer should be the replaced message string`() { assertEquals(replacedMessageString, message.formatted(locales, commandManager.consoleCommandIssuer)) } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + verify(sender).sendMessage("§9§9$replacedMessageString") + } + } } @Nested @@ -156,5 +225,32 @@ class LocalizationTest : TestWithMockBukkit() { assertThat(message.formatted(locales), !containsSubstring(replacementKey)) assertThat(message.formatted(locales), containsSubstring(replacementValue)) } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + val sentMessage = argumentCaptor { + verify(sender).sendMessage(capture()) + }.firstValue + + assertNotEquals(replacedMessageString, sentMessage) + assertThat(sentMessage, !containsSubstring(replacementKey)) + assertThat(sentMessage, containsSubstring(replacementValue)) + } + } } } From 2cb134bdebe91f0d6eea72ccd590f7149da26dd2 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 23:58:41 -0400 Subject: [PATCH 7/9] Add base exception class MultiverseException. Allows for localized messages to be contained in our exceptions. --- .../exceptions/MultiverseException.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java diff --git a/src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java b/src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java new file mode 100644 index 00000000..268468fd --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java @@ -0,0 +1,52 @@ +package com.onarandombox.MultiverseCore.exceptions; + +import com.onarandombox.MultiverseCore.commandtools.MVCommandIssuer; +import com.onarandombox.MultiverseCore.utils.message.Message; +import org.jetbrains.annotations.Nullable; + +/** + * A base exception for Multiverse. + *
+ * {@link #getMVMessage()} provides access to a {@link Message} which can be used to provide a localized message. See + * {@link MVCommandIssuer#sendInfo(Message)}. + */ +public class MultiverseException extends Exception { + + private final @Nullable Message message; + + /** + * Creates a new exception with the given message and cause. + *
+ * If the message is not null, this exception will also contain a {@link Message} which can be accessed via + * {@link #getMVMessage()}. This message will just be the given message wrapped in a {@link Message}. + * + * @param message The message for the exception + * @param cause The cause of the exception + */ + public MultiverseException(@Nullable String message, @Nullable Throwable cause) { + this(message != null ? Message.of(message) : null, cause); + } + + /** + * Creates a new exception with the given message and cause. + *
+ * If the message is not null, this exception will also contain a String message which can be accessed via + * {@link #getMessage()}. This message will just be the given message formatted without locale support. + * + * @param message The message for the exception + * @param cause The cause of the exception + */ + public MultiverseException(@Nullable Message message, @Nullable Throwable cause) { + super(message != null ? message.formatted() : null, cause); + this.message = message; + } + + /** + * Gets the {@link Message} for this exception. + * + * @return The message, or null if none was provided + */ + public final @Nullable Message getMVMessage() { + return message; + } +} From 5dcc0651e4c163c1e8bee98423b92db57ea23ff5 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Wed, 29 Mar 2023 23:59:19 -0400 Subject: [PATCH 8/9] Add MVCorei18n#bundle for creating Message objects. --- .../com/onarandombox/MultiverseCore/utils/MVCorei18n.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java b/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java index 1dc79b9a..1e5cbe35 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java @@ -2,6 +2,9 @@ package com.onarandombox.MultiverseCore.utils; import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.message.Message; +import com.onarandombox.MultiverseCore.utils.message.MessageReplacement; +import org.jetbrains.annotations.NotNull; public enum MVCorei18n implements MessageKeyProvider { // config status @@ -84,4 +87,9 @@ public enum MVCorei18n implements MessageKeyProvider { public MessageKey getMessageKey() { return this.key; } + + @NotNull + public Message bundle(@NotNull String nonLocalizedMessage, @NotNull MessageReplacement... replacements) { + return Message.of(this, nonLocalizedMessage, replacements); + } } From dab8ac2bf8da40513bd1f103907c0a74cff84dc1 Mon Sep 17 00:00:00 2001 From: Jeremy Wood Date: Thu, 30 Mar 2023 00:14:45 -0400 Subject: [PATCH 9/9] Set up locales before registering commands. --- .../java/com/onarandombox/MultiverseCore/MultiverseCore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java b/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java index 31e2cfee..db189e0c 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java +++ b/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java @@ -132,8 +132,8 @@ public class MultiverseCore extends JavaPlugin implements MVCore { // Init all the other stuff this.loadAnchors(); this.registerEvents(); - this.registerCommands(); this.setUpLocales(); + this.registerCommands(); this.registerDestinations(); this.setupMetrics(); this.loadPlaceholderAPIIntegration();