package fr.xephi.authme.message.updater; import ch.jalu.configme.configurationdata.ConfigurationData; import ch.jalu.configme.configurationdata.PropertyListBuilder; import ch.jalu.configme.properties.Property; import ch.jalu.configme.properties.StringProperty; import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; import ch.jalu.configme.resource.PropertyReader; import ch.jalu.configme.resource.PropertyResource; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.util.FileUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static java.util.Collections.singletonList; /** * Migrates the used messages file to a complete, up-to-date version when necessary. */ public class MessageUpdater { private ConsoleLogger logger = ConsoleLoggerFactory.get(MessageUpdater.class); /** * Applies any necessary migrations to the user's messages file and saves it if it has been modified. * * @param userFile the user's messages file (yml file in the plugin's folder) * @param localJarPath path to the messages file in the JAR for the same language (may not exist) * @param defaultJarPath path to the messages file in the JAR for the default language * @return true if the file has been migrated and saved, false if it is up-to-date */ public boolean migrateAndSave(File userFile, String localJarPath, String defaultJarPath) { JarMessageSource jarMessageSource = new JarMessageSource(localJarPath, defaultJarPath); return migrateAndSave(userFile, jarMessageSource); } /** * Performs the migration. * * @param userFile the file to verify and migrate * @param jarMessageSource jar message source to get texts from if missing * @return true if the file has been migrated and saved, false if it is up-to-date */ private boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) { // YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe MessageKeyConfigurationData configurationData = createConfigurationData(); PropertyResource userResource = new MigraterYamlFileResource(userFile); PropertyReader reader = userResource.createReader(); configurationData.initializeValues(reader); // Step 1: Migrate any old keys in the file to the new paths boolean movedOldKeys = migrateOldKeys(reader, configurationData); // Step 2: Perform newer migrations boolean movedNewerKeys = migrateKeys(reader, configurationData); // Step 3: Take any missing messages from the message files shipped in the AuthMe JAR boolean addedMissingKeys = addMissingKeys(jarMessageSource, configurationData); if (movedOldKeys || movedNewerKeys || addedMissingKeys) { backupMessagesFile(userFile); userResource.exportProperties(configurationData); logger.debug("Successfully saved {0}", userFile); return true; } return false; } private boolean migrateKeys(PropertyReader propertyReader, MessageKeyConfigurationData configurationData) { return moveIfApplicable(propertyReader, configurationData, "misc.two_factor_create", MessageKey.TWO_FACTOR_CREATE); } private static boolean moveIfApplicable(PropertyReader reader, MessageKeyConfigurationData configurationData, String oldPath, MessageKey messageKey) { if (configurationData.getMessage(messageKey) == null && reader.getString(oldPath) != null) { configurationData.setMessage(messageKey, reader.getString(oldPath)); return true; } return false; } private boolean migrateOldKeys(PropertyReader propertyReader, MessageKeyConfigurationData configurationData) { boolean hasChange = OldMessageKeysMigrater.migrateOldPaths(propertyReader, configurationData); if (hasChange) { logger.info("Old keys have been moved to the new ones in your messages_xx.yml file"); } return hasChange; } private boolean addMissingKeys(JarMessageSource jarMessageSource, MessageKeyConfigurationData configurationData) { List addedKeys = new ArrayList<>(); for (Property property : configurationData.getAllMessageProperties()) { final String key = property.getPath(); if (configurationData.getValue(property) == null) { configurationData.setValue(property, jarMessageSource.getMessageFromJar(property)); addedKeys.add(key); } } if (!addedKeys.isEmpty()) { logger.info( "Added " + addedKeys.size() + " missing keys to your messages_xx.yml file: " + addedKeys); return true; } return false; } private static void backupMessagesFile(File messagesFile) { String backupName = FileUtils.createBackupFilePath(messagesFile); File backupFile = new File(backupName); try { Files.copy(messagesFile, backupFile); } catch (IOException e) { throw new IllegalStateException("Could not back up '" + messagesFile + "' to '" + backupFile + "'", e); } } /** * Constructs the {@link ConfigurationData} for exporting a messages file in its entirety. * * @return the configuration data to export with */ public static MessageKeyConfigurationData createConfigurationData() { Map comments = ImmutableMap.builder() .put("registration", "Registration") .put("password", "Password errors on registration") .put("login", "Login") .put("error", "Errors") .put("antibot", "AntiBot") .put("unregister", "Unregister") .put("misc", "Other messages") .put("session", "Session messages") .put("on_join_validation", "Error messages when joining") .put("email", "Email") .put("recovery", "Password recovery by email") .put("captcha", "Captcha") .put("verification", "Verification code") .put("time", "Time units") .put("two_factor", "Two-factor authentication") .put("bedrock_auto_login", "3rd party features: Bedrock Auto Login") .put("login_location_fix", "3rd party features: Login Location Fix") .put("double_login_fix", "3rd party features: Double Login Fix") .build(); Set addedKeys = new HashSet<>(); MessageKeyPropertyListBuilder builder = new MessageKeyPropertyListBuilder(); // Add one key per section based on the comments map above so that the order is clear for (String path : comments.keySet()) { MessageKey key = Arrays.stream(MessageKey.values()).filter(p -> p.getKey().startsWith(path + ".")) .findFirst().orElseThrow(() -> new IllegalStateException(path)); builder.addMessageKey(key); addedKeys.add(key.getKey()); } // Add all remaining keys to the property list builder Arrays.stream(MessageKey.values()) .filter(key -> !addedKeys.contains(key.getKey())) .forEach(builder::addMessageKey); // Create ConfigurationData instance Map> commentsMap = comments.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> singletonList(e.getValue()))); return new MessageKeyConfigurationData(builder, commentsMap); } static final class MessageKeyProperty extends StringProperty { MessageKeyProperty(MessageKey messageKey) { super(messageKey.getKey(), ""); } @Override protected String getFromReader(PropertyReader reader, ConvertErrorRecorder errorRecorder) { return reader.getString(getPath()); } } static final class MessageKeyPropertyListBuilder { private PropertyListBuilder propertyListBuilder = new PropertyListBuilder(); void addMessageKey(MessageKey key) { propertyListBuilder.add(new MessageKeyProperty(key)); } @SuppressWarnings("unchecked") List getAllProperties() { return (List) propertyListBuilder.create(); } } }