From 760a2a909cfdd33cd55e24c2b0a15eae0ce198ad Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 29 Jan 2018 21:46:58 +0100 Subject: [PATCH] #1467 Fix export issues (style, encoding) - Override yaml file resource to ensure that lines aren't wrapped - Override yaml file reader to ensure the file is always read as UTF-8 --- .../MessageMigraterPropertyReader.java | 131 ++++++++++++++++++ .../message/updater/MessageUpdater.java | 55 +++++++- 2 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java diff --git a/src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java b/src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java new file mode 100644 index 000000000..400b25ff3 --- /dev/null +++ b/src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java @@ -0,0 +1,131 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.configme.resource.PropertyReader; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Duplication of ConfigMe's {@link ch.jalu.configme.resource.YamlFileReader} with a character encoding + * fix in {@link #reload}. + */ +public class MessageMigraterPropertyReader implements PropertyReader { + + private final File file; + private Map root; + /** See same field in {@link ch.jalu.configme.resource.YamlFileReader} for details. */ + private boolean hasObjectAsRoot = false; + + /** + * Constructor. + * + * @param file the file to load + */ + public MessageMigraterPropertyReader(File file) { + this.file = file; + reload(); + } + + @Override + public Object getObject(String path) { + if (path.isEmpty()) { + return hasObjectAsRoot ? root.get("") : root; + } + Object node = root; + String[] keys = path.split("\\."); + for (String key : keys) { + node = getIfIsMap(key, node); + if (node == null) { + return null; + } + } + return node; + } + + @Override + public T getTypedObject(String path, Class clazz) { + Object value = getObject(path); + if (clazz.isInstance(value)) { + return clazz.cast(value); + } + return null; + } + + @Override + public void set(String path, Object value) { + Objects.requireNonNull(path); + + if (path.isEmpty()) { + root.clear(); + root.put("", value); + hasObjectAsRoot = true; + } else if (hasObjectAsRoot) { + throw new ConfigMeException("The root path is a bean property; you cannot set values to any subpath. " + + "Modify the bean at the root or set a new one instead."); + } else { + setValueInChildPath(path, value); + } + } + + /** + * Sets the value at the given path. This method is used when the root is a map and not a specific object. + * + * @param path the path to set the value at + * @param value the value to set + */ + @SuppressWarnings("unchecked") + private void setValueInChildPath(String path, Object value) { + Map node = root; + String[] keys = path.split("\\."); + for (int i = 0; i < keys.length - 1; ++i) { + Object child = node.get(keys[i]); + if (child instanceof Map) { + node = (Map) child; + } else { // child is null or some other value - replace with map + Map newEntry = new HashMap<>(); + node.put(keys[i], newEntry); + if (value == null) { + // For consistency, replace whatever value/null here with an empty map, + // but if the value is null our work here is done. + return; + } + node = newEntry; + } + } + // node now contains the parent map (existing or newly created) + if (value == null) { + node.remove(keys[keys.length - 1]); + } else { + node.put(keys[keys.length - 1], value); + } + } + + @Override + public void reload() { + try (FileInputStream fis = new FileInputStream(file); + InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) { + + Object obj = new Yaml().load(isr); + root = obj == null ? new HashMap<>() : (Map) obj; + } catch (IOException e) { + throw new ConfigMeException("Could not read file '" + file + "'", e); + } catch (ClassCastException e) { + throw new ConfigMeException("Top-level is not a map in '" + file + "'", e); + } + } + + private static Object getIfIsMap(String key, Object value) { + if (value instanceof Map) { + return ((Map) value).get(key); + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java index 2fc6fb0f5..1c0721082 100644 --- a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java +++ b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java @@ -1,12 +1,16 @@ package fr.xephi.authme.message.updater; import ch.jalu.configme.SettingsManager; +import ch.jalu.configme.beanmapper.leafproperties.LeafPropertiesGenerator; +import ch.jalu.configme.configurationdata.PropertyListBuilder; import ch.jalu.configme.properties.Property; import ch.jalu.configme.properties.StringProperty; import ch.jalu.configme.resource.YamlFileResource; import com.google.common.collect.ImmutableList; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.message.MessageKey; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; import java.io.File; import java.util.List; @@ -40,7 +44,7 @@ public class MessageUpdater { */ boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) { // YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe - YamlFileResource userResource = new YamlFileResource(userFile); + YamlFileResource userResource = new MigraterYamlFileResource(userFile); SettingsManager settingsManager = SettingsManager.createWithProperties(userResource, null, TEXT_PROPERTIES); // Step 1: Migrate any old keys in the file to the new paths @@ -81,10 +85,53 @@ public class MessageUpdater { } private static List> buildPropertyEntriesForMessageKeys() { - ImmutableList.Builder> listBuilder = ImmutableList.builder(); + StringPropertyListBuilder builder = new StringPropertyListBuilder(); for (MessageKey messageKey : MessageKey.values()) { - listBuilder.add(new StringProperty(messageKey.getKey(), "")); + builder.add(messageKey.getKey()); + } + return ImmutableList.copyOf(builder.create()); + } + + /** + * Wraps a {@link PropertyListBuilder} for easier construction of string properties. + * ConfigMe's property list builder ensures that properties are grouped together by path. + */ + private static final class StringPropertyListBuilder { + private PropertyListBuilder propertyListBuilder = new PropertyListBuilder(); + + void add(String path) { + propertyListBuilder.add(new StringProperty(path, "")); + } + + @SuppressWarnings("unchecked") + List> create() { + return (List) propertyListBuilder.create(); + } + } + + /** + * Extension of {@link YamlFileResource} to fine-tune the export style. + */ + private static final class MigraterYamlFileResource extends YamlFileResource { + + private Yaml singleQuoteYaml; + + MigraterYamlFileResource(File file) { + super(file, new MessageMigraterPropertyReader(file), new LeafPropertiesGenerator()); + } + + @Override + protected Yaml getSingleQuoteYaml() { + if (singleQuoteYaml == null) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setAllowUnicode(true); + options.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED); + // Overridden setting: don't split lines + options.setSplitLines(false); + singleQuoteYaml = new Yaml(options); + } + return singleQuoteYaml; } - return listBuilder.build(); } }