diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index edbbdb20..598f8d3a 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -11,6 +11,7 @@ import fr.xephi.authme.command.executable.authme.ForceLoginCommand; import fr.xephi.authme.command.executable.authme.GetEmailCommand; import fr.xephi.authme.command.executable.authme.GetIpCommand; import fr.xephi.authme.command.executable.authme.LastLoginCommand; +import fr.xephi.authme.command.executable.authme.MessagesCommand; import fr.xephi.authme.command.executable.authme.PurgeBannedPlayersCommand; import fr.xephi.authme.command.executable.authme.PurgeCommand; import fr.xephi.authme.command.executable.authme.PurgeLastPositionCommand; @@ -288,6 +289,15 @@ public class CommandInitializer { .executableCommand(ConverterCommand.class) .build(); + CommandDescription.builder() + .parent(AUTHME_BASE) + .labels("messages") + .description("Add missing messages") + .detailedDescription("Adds missing messages to the current messages file.") + // TODO #768: add permission for command + .executableCommand(MessagesCommand.class) + .build(); + // Register the base login command final CommandDescription LOGIN_BASE = CommandDescription.builder() .parent(null) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/MessagesCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/MessagesCommand.java new file mode 100644 index 00000000..25e0bf5b --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/MessagesCommand.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.service.MessageUpdater; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.io.File; +import java.util.List; + +/** + * Messages command, updates the user's messages file with any missing files + * from the provided file in the JAR. + */ +public class MessagesCommand implements ExecutableCommand { + + private static final String DEFAULT_LANGUAGE = "en"; + + @Inject + private Settings settings; + @Inject + @DataFolder + private File dataFolder; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + final String language = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE); + + try { + new MessageUpdater( + new File(dataFolder, getMessagePath(language)), + getMessagePath(language), + getMessagePath(DEFAULT_LANGUAGE)) + .executeCopy(sender); + } catch (Exception e) { + sender.sendMessage("Could not update messages: " + e.getMessage()); + ConsoleLogger.logException("Could not update messages:", e); + } + } + + private static String getMessagePath(String code) { + return "messages/messages_" + code + ".yml"; + } +} diff --git a/src/main/java/fr/xephi/authme/service/MessageUpdater.java b/src/main/java/fr/xephi/authme/service/MessageUpdater.java new file mode 100644 index 00000000..a94922e8 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/MessageUpdater.java @@ -0,0 +1,108 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.util.FileUtils; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Updates a user's messages file with messages from the JAR files. + */ +public class MessageUpdater { + + private final File userFile; + private final FileConfiguration userConfiguration; + private final FileConfiguration localJarConfiguration; + private final FileConfiguration defaultJarConfiguration; + private boolean hasMissingMessages = false; + + public MessageUpdater(File userFile, String jarFile, String jarDefaultsFile) throws Exception { + if (!userFile.exists()) { + throw new Exception("Local messages file does not exist"); + } + this.userFile = userFile; + this.userConfiguration = YamlConfiguration.loadConfiguration(userFile); + + localJarConfiguration = loadJarFileOrSendError(jarFile); + defaultJarConfiguration = jarFile.equals(jarDefaultsFile) + ? null + : loadJarFileOrSendError(jarDefaultsFile); + if (localJarConfiguration == null && defaultJarConfiguration == null) { + throw new Exception("Could not load any JAR messages file to copy from"); + } + } + + public void executeCopy(CommandSender sender) { + copyMissingMessages(); + + if (!hasMissingMessages) { + sender.sendMessage("No new messages to add"); + return; + } + + // Save user configuration file + try { + userConfiguration.save(userFile); + sender.sendMessage("Message file updated with new messages"); + } catch (IOException e) { + sender.sendMessage("Could not save to messages file"); + ConsoleLogger.logException("Could not save new messages to file:", e); + } + } + + private void copyMissingMessages() { + for (MessageKey entry : MessageKey.values()) { + final String key = entry.getKey(); + if (!userConfiguration.contains(key)) { + String jarMessage = getMessageFromJar(key); + if (jarMessage != null) { + hasMissingMessages = true; + userConfiguration.set(key, jarMessage); + } + } + } + } + + private String getMessageFromJar(String key) { + String message = (localJarConfiguration == null ? null : localJarConfiguration.getString(key)); + if (message != null) { + return message; + } + return (defaultJarConfiguration == null ? null : defaultJarConfiguration.getString(key)); + } + + private static FileConfiguration loadJarFileOrSendError(String jarPath) { + try (InputStream stream = FileUtils.getResourceFromJar(jarPath)) { + if (stream == null) { + ConsoleLogger.info("Could not load '" + jarPath + "' from JAR"); + return null; + } + InputStreamReader isr = new InputStreamReader(stream); + FileConfiguration configuration = YamlConfiguration.loadConfiguration(isr); + close(isr); + return configuration; + } catch (IOException e) { + ConsoleLogger.logException("Exception while handling JAR path '" + jarPath + "'", e); + } + return null; + } + + private static void close(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + ConsoleLogger.info("Cannot close '" + closeable + "': " + StringUtils.formatException(e)); + } + } + } +} diff --git a/src/main/java/fr/xephi/authme/util/FileUtils.java b/src/main/java/fr/xephi/authme/util/FileUtils.java index 831beafd..86c8cae4 100644 --- a/src/main/java/fr/xephi/authme/util/FileUtils.java +++ b/src/main/java/fr/xephi/authme/util/FileUtils.java @@ -23,7 +23,7 @@ public final class FileUtils { * Copy a resource file (from the JAR) to the given file if it doesn't exist. * * @param destinationFile The file to check and copy to (outside of JAR) - * @param resourcePath Absolute path to the resource file (path to file within JAR) + * @param resourcePath Local path to the resource file (path to file within JAR) * * @return False if the file does not exist and could not be copied, true otherwise */ @@ -35,9 +35,7 @@ public final class FileUtils { return false; } - // ClassLoader#getResourceAsStream does not deal with the '\' path separator: replace to '/' - final String normalizedPath = resourcePath.replace("\\", "/"); - try (InputStream is = AuthMe.class.getClassLoader().getResourceAsStream(normalizedPath)) { + try (InputStream is = getResourceFromJar(resourcePath)) { if (is == null) { ConsoleLogger.warning(format("Cannot copy resource '%s' to file '%s': cannot load resource", resourcePath, destinationFile.getPath())); @@ -52,6 +50,18 @@ public final class FileUtils { return false; } + /** + * Returns a JAR file as stream. Returns null if it doesn't exist. + * + * @param path the local path (starting from resources project, e.g. "config.yml" for 'resources/config.yml') + * @return the stream if the file exists, or false otherwise + */ + public static InputStream getResourceFromJar(String path) { + // ClassLoader#getResourceAsStream does not deal with the '\' path separator: replace to '/' + final String normalizedPath = path.replace("\\", "/"); + return AuthMe.class.getClassLoader().getResourceAsStream(normalizedPath); + } + /** * Delete a given directory and all its content. * diff --git a/src/test/java/fr/xephi/authme/util/FileUtilsTest.java b/src/test/java/fr/xephi/authme/util/FileUtilsTest.java index f36acf75..e7a8d873 100644 --- a/src/test/java/fr/xephi/authme/util/FileUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/FileUtilsTest.java @@ -11,6 +11,8 @@ import java.io.File; import java.io.IOException; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; /** @@ -119,6 +121,13 @@ public class FileUtilsTest { // Nothing happens } + @Test + public void shouldGetResourceFromJar() { + // given / when / then + assertThat(FileUtils.getResourceFromJar("config.yml"), not(nullValue())); + assertThat(FileUtils.getResourceFromJar("does-not-exist"), nullValue()); + } + @Test public void shouldConstructPath() { // given/when