From 7d41ccbc9ca92feb23c71dfe2ff2b76ce4c0d14f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 3 Jan 2016 15:22:32 +0100 Subject: [PATCH] Settings: use class constants for properties, create custom writer - Create Property class for defining config properties - Create logic for typed retrival of properties from YAML file - Add custom save method - Retain comments from Comment annotations in the classes - Write in a sorted order: first discovered properties are first written to config.yml - Adjust properties to reflect the current config.yml - Add sample tests for the retrieval and writing of properties with the new setup --- .../settings/custom/ConverterSettings.java | 50 ++--- .../authme/settings/custom/CustomSetting.java | 156 ---------------- .../settings/custom/DatabaseSettings.java | 120 ++++++------ .../authme/settings/custom/EmailSettings.java | 97 +++++----- .../authme/settings/custom/HooksSettings.java | 57 +++--- .../authme/settings/custom/NewSetting.java | 148 +++++++++++++++ .../settings/custom/ProtectionSettings.java | 71 +++----- .../authme/settings/custom/PurgeSettings.java | 65 +++---- .../settings/custom/SecuritySettings.java | 73 ++++---- .../settings/custom/annotations/Type.java | 43 ----- .../{annotations => domain}/Comment.java | 11 +- .../settings/custom/domain/Property.java | 100 ++++++++++ .../settings/custom/domain/PropertyType.java | 146 +++++++++++++++ .../settings/custom/domain/SettingsClass.java | 7 + .../settings/custom/propertymap/Node.java | 121 ++++++++++++ .../custom/propertymap/PropertyMap.java | 48 +++++ .../propertymap/PropertyMapComparator.java | 39 ++++ .../fr/xephi/authme/util/CollectionUtils.java | 14 ++ .../fr/xephi/authme/util/StringUtils.java | 8 + .../settings/custom/NewSettingTest.java | 111 +++++++++++ .../settings/custom/NewSettingsWriteTest.java | 42 +++++ src/test/resources/437-config-test.yml | 44 +++++ src/test/resources/437-write-test.yml | 172 ++++++++++++++++++ 23 files changed, 1225 insertions(+), 518 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/NewSetting.java delete mode 100644 src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java rename src/main/java/fr/xephi/authme/settings/custom/{annotations => domain}/Comment.java (62%) create mode 100644 src/main/java/fr/xephi/authme/settings/custom/domain/Property.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java create mode 100644 src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java create mode 100644 src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java create mode 100644 src/test/resources/437-config-test.yml create mode 100644 src/test/resources/437-write-test.yml diff --git a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java index 7cee6a521..c9f1ba99e 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java @@ -1,46 +1,32 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -public class ConverterSettings extends CustomSetting { +public class ConverterSettings implements SettingsClass { @Comment("Rakamak file name") - @Type(SettingType.String) - public String rakamakFileName = "users.rak"; + public static final Property RAKAMAK_FILE_NAME = + newProperty(STRING, "Converter.Rakamak.fileName", "users.rak"); - @Comment("Rakamak use Ip ?") - @Type(SettingType.Boolean) - public boolean rakamakeUseIP = false; + @Comment("Rakamak use IP?") + public static final Property RAKAMAK_USE_IP = + newProperty(BOOLEAN, "Converter.Rakamak.useIP", false); @Comment("Rakamak IP file name") - @Type(SettingType.String) - public String rakamakIPFileName = "UsersIp.rak"; + public static final Property RAKAMAK_IP_FILE_NAME = + newProperty(STRING, "Converter.Rakamak.ipFileName", "UsersIp.rak"); @Comment("CrazyLogin database file name") - @Type(SettingType.String) - public String crazyLoginFileName = "accounts.db"; + public static final Property CRAZYLOGIN_FILE_NAME = + newProperty(STRING, "Converter.CrazyLogin.fileName", "accounts.db"); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "converter.yml"); + private ConverterSettings() { + } - private ConverterSettings instance; - - public ConverterSettings() - { - super(configFile); - instance = this; - } - - public ConverterSettings getInstance() { - return instance; - } - - public void setInstance(ConverterSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java b/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java deleted file mode 100644 index 27792efd9..000000000 --- a/src/main/java/fr/xephi/authme/settings/custom/CustomSetting.java +++ /dev/null @@ -1,156 +0,0 @@ -package fr.xephi.authme.settings.custom; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.List; - -import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.settings.CustomConfiguration; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; - -public class CustomSetting extends CustomConfiguration { - - private File configFile; - public boolean isFirstLaunch = false; - - public CustomSetting(File file) { - super(file); - this.configFile = file; - try { - if (!configFile.exists()) - { - isFirstLaunch = true; - configFile.createNewFile(); - } - else - { - load(); - loadValues(); - } - save(); - } catch (IOException e) - { - ConsoleLogger.writeStackTrace(e); - } - } - - @Override - public boolean reLoad() - { - boolean out = true; - if (!configFile.exists()) { - try { - configFile.createNewFile(); - save(); - } catch (IOException e) { - out = false; - } - } - if (out) - { - load(); - loadValues(); - } - return out; - } - - public void loadValues() - { - for (Field f : this.getClass().getDeclaredFields()) - { - if (!f.isAnnotationPresent(Type.class)) - continue; - f.setAccessible(true); - try { - switch (f.getAnnotation(Type.class).value()) - { - case Boolean: - f.setBoolean(this, this.getBoolean(f.getName())); - break; - case Double: - f.setDouble(this, this.getDouble(f.getName())); - break; - case Int: - f.setInt(this, this.getInt(f.getName())); - break; - case Long: - f.setLong(this, this.getLong(f.getName())); - break; - case String: - f.set(this, this.getString(f.getName())); - break; - case StringList: - f.set(this, this.getStringList(f.getName())); - break; - default: - break; - } - } catch (Exception e) - { - ConsoleLogger.writeStackTrace(e); - } - } - } - - @Override - public void save() - { - FileWriter writer = null; - try { - writer = new FileWriter(configFile); - writer.write(""); - for (Field f : this.getClass().getDeclaredFields()) - { - if (!f.isAnnotationPresent(Comment.class)) - continue; - if (!f.isAnnotationPresent(Type.class)) - continue; - for (String s : f.getAnnotation(Comment.class).value()) - { - writer.append("# " + s + "\n"); - } - writer.append(f.getName() + ": "); - switch (f.getAnnotation(Type.class).value()) - { - case Boolean: - writer.append(f.getBoolean(this) ? "true" : "false"); - break; - case Double: - writer.append("" + f.getDouble(this)); - break; - case Int: - writer.append("" + f.getInt(this)); - break; - case String: - writer.append("'" + f.get(this).toString() + "'"); - break; - case StringList: - @SuppressWarnings("unchecked") - List list = (List) f.get(this); - writer.append("\n"); - if (list.isEmpty()) - writer.write("[]"); - else - for (String s : list) - writer.append(" - '" + s + "'\n"); - break; - case Long: - writer.append("" + f.getLong(this)); - break; - default: - break; - - } - writer.append("\n"); - writer.flush(); - } - writer.close(); - } catch (Exception e) { - ConsoleLogger.writeStackTrace(e); - } - } - -} diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java index c3c20c6bd..26e36310f 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -1,121 +1,109 @@ package fr.xephi.authme.settings.custom; -import java.io.File; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -public class DatabaseSettings extends CustomSetting { +public class DatabaseSettings implements SettingsClass { @Comment({"What type of database do you want to use?", "Valid values: sqlite, mysql"}) - @Type(SettingType.String) - public String backend = "sqlite"; + public static final Property BACKEND = + newProperty(STRING, "DataSource.backend", "sqlite"); @Comment("Enable database caching, should improve database performance") - @Type(SettingType.Boolean) - public boolean caching = true; + public static final Property USE_CACHING = + newProperty(BOOLEAN, "DataSource.caching", true); @Comment("Database host address") - @Type(SettingType.String) - public String mySQLHost = "127.0.0.1"; + public static final Property MYSQL_HOST = + newProperty(STRING, "DataSource.mySQLHost", "127.0.0.1"); @Comment("Database port") - @Type(SettingType.String) - public String mySQLPort = "3306"; + public static final Property MYSQL_PORT = + newProperty(STRING, "DataSource.mySQLPort", "3306"); @Comment("Username about Database Connection Infos") - @Type(SettingType.String) - public String mySQLUsername = "authme"; + public static final Property MYSQL_USERNAME = + newProperty(STRING, "DataSource.mySQLUsername", "authme"); @Comment("Password about Database Connection Infos") - @Type(SettingType.String) - public String mySQLPassword = "12345"; + public static final Property MYSQL_PASSWORD = + newProperty(STRING, "DataSource.mySQLPassword", "123456"); @Comment("Database Name, use with converters or as SQLITE database name") - @Type(SettingType.String) - public String mySQLDatabase = "authme"; + public static final Property MYSQL_DATABASE = + newProperty(STRING, "DataSource.mySQLDatabase", "authme"); @Comment("Table of the database") - @Type(SettingType.String) - public String mySQLTablename = "authme"; + public static final Property MYSQL_TABLE = + newProperty(STRING, "DataSource.mySQLTablename", "authme"); @Comment("Column of IDs to sort data") - @Type(SettingType.String) - public String mySQLColumnId = "id"; + public static final Property MYSQL_COL_ID = + newProperty(STRING, "DataSource.mySQLColumnId", "id"); @Comment("Column for storing or checking players nickname") - @Type(SettingType.String) - public String mySQLColumnName = "username"; + public static final Property MYSQL_COL_NAME = + newProperty(STRING, "DataSource.mySQLColumnName", "username"); @Comment("Column for storing or checking players RealName ") - @Type(SettingType.String) - public String mySQLColumnRealName = "realname"; + public static final Property MYSQL_COL_REALNAME = + newProperty(STRING, "DataSource.mySQLRealName", "realname"); @Comment("Column for storing players passwords") - @Type(SettingType.String) - public String mySQLColumnPassword = "password"; + public static final Property MYSQL_COL_PASSWORD = + newProperty(STRING, "DataSource.mySQLColumnPassword", "password"); @Comment("Column for storing players passwords salts") - @Type(SettingType.String) - public String mySQLColumnSalt = ""; + public static final Property MYSQL_COL_SALT = + newProperty(STRING, "ExternalBoardOptions.mySQLColumnSalt", ""); @Comment("Column for storing players emails") - @Type(SettingType.String) - public String mySQLColumnEmail = "email"; + public static final Property MYSQL_COL_EMAIL = + newProperty(STRING, "DataSource.mySQLColumnEmail", "email"); @Comment("Column for storing if a player is logged in or not") - @Type(SettingType.String) - public String mySQLColumnLogged = "isLogged"; + public static final Property MYSQL_COL_ISLOGGED = + newProperty(STRING, "DataSource.mySQLColumnLogged", "isLogged"); @Comment("Column for storing players ips") - @Type(SettingType.String) - public String mySQLColumnIp = "ip"; + public static final Property MYSQL_COL_IP = + newProperty(STRING, "DataSource.mySQLColumnIp", "ip"); @Comment("Column for storing players lastlogins") - @Type(SettingType.String) - public String mySQLColumnLastLogin = "lastlogin"; + public static final Property MYSQL_COL_LASTLOGIN = + newProperty(STRING, "DataSource.mySQLColumnLastLogin", "lastlogin"); @Comment("Column for storing player LastLocation - X") - @Type(SettingType.String) - public String mySQLColumnLastLocX = "x"; + public static final Property MYSQL_COL_LASTLOC_X = + newProperty(STRING, "DataSource.mySQLlastlocX", "x"); @Comment("Column for storing player LastLocation - Y") - @Type(SettingType.String) - public String mySQLColumnLastLocY = "y"; + public static final Property MYSQL_COL_LASTLOC_Y = + newProperty(STRING, "DataSource.mySQLlastlocY", "y"); @Comment("Column for storing player LastLocation - Z") - @Type(SettingType.String) - public String mySQLColumnLastLocZ = "z"; + public static final Property MYSQL_COL_LASTLOC_Z = + newProperty(STRING, "DataSource.mySQLlastlocZ", "z"); @Comment("Column for storing player LastLocation - World Name") - @Type(SettingType.String) - public String mySQLColumnLastLocWorld = "world"; + public static final Property MYSQL_COL_LASTLOC_WORLD = + newProperty(STRING, "DataSource.mySQLlastlocWorld", "world"); @Comment("Column for storing players groups") - @Type(SettingType.String) - public String mySQLColumnGroup = ""; + public static final Property MYSQL_COL_GROUP = + newProperty(STRING, "ExternalBoardOptions.mySQLColumnGroup", ""); @Comment("Enable this when you allow registration through a website") - @Type(SettingType.Boolean) - public boolean mySQLWebsite = false; + public static final Property MYSQL_WEBSITE = + newProperty(BOOLEAN, "DataSource.mySQLWebsite", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "database.yml"); - - private DatabaseSettings instance; - - public DatabaseSettings() - { - super(configFile); - instance = this; + private DatabaseSettings() { } - public DatabaseSettings getInstance() { - return instance; - } - - public void setInstance(DatabaseSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java index d6cc36753..f53d9549d 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java @@ -1,83 +1,72 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; + import java.util.List; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING_LIST; -public class EmailSettings extends CustomSetting { +public class EmailSettings implements SettingsClass { @Comment("Email SMTP server host") - @Type(SettingType.String) - public String mailSMTP = "smtp.gmail.com"; + public static final Property SMTP_HOST = + newProperty(STRING, "Email.mailSMTP", "smtp.gmail.com"); @Comment("Email SMTP server port") - @Type(SettingType.Int) - public int mailPort = 465; + public static final Property SMTP_PORT = + newProperty(INTEGER, "Email.mailPort", 465); - @Comment("Email account whose send the mail") - @Type(SettingType.String) - public String mailAccount = ""; + @Comment("Email account which sends the mails") + public static final Property MAIL_ACCOUNT = + newProperty(STRING, "Email.mailAccount", ""); @Comment("Email account password") - @Type(SettingType.String) - public String mailPassword = ""; + public static final Property MAIL_PASSWORD = + newProperty(STRING, "Email.mailPassword", ""); - @Comment("Random password length") - @Type(SettingType.Int) - public int recoveryPasswordLength = 8; + @Comment("Recovery password length") + public static final Property RECOVERY_PASSWORD_LENGTH = + newProperty(INTEGER, "Email.RecoveryPasswordLength", 8); @Comment("Mail Subject") - @Type(SettingType.String) - public String mailSubject = "Your new AuthMe password"; + public static final Property RECOVERY_MAIL_SUBJECT = + newProperty(STRING, "Email.mailSubject", "Your new AuthMe password"); @Comment("Like maxRegPerIP but with email") - @Type(SettingType.Int) - public int maxRegPerEmail = 1; + public static final Property MAX_REG_PER_EMAIL = + newProperty(INTEGER, "Email.maxRegPerEmail", 1); - @Comment("Recall players to add an email ?") - @Type(SettingType.Boolean) - public boolean recallPlayers = false; + @Comment("Recall players to add an email?") + public static final Property RECALL_PLAYERS = + newProperty(BOOLEAN, "Email.recallPlayers", false); @Comment("Delay in minute for the recall scheduler") - @Type(SettingType.Int) - public int delayRecall = 5; + public static final Property DELAY_RECALL = + newProperty(INTEGER, "Email.delayRecall", 5); @Comment("Blacklist these domains for emails") - @Type(SettingType.StringList) - public List emailBlackListed = new ArrayList(); + public static final Property> DOMAIN_BLACKLIST = + newProperty(STRING_LIST, "Email.emailBlacklisted", "10minutemail.com"); @Comment("Whitelist ONLY these domains for emails") - @Type(SettingType.StringList) - public List emailWhiteListed = new ArrayList(); + public static final Property> DOMAIN_WHITELIST = + newProperty(STRING_LIST, "Email.emailWhitelisted"); - @Comment("Do we need to send new password draw in an image ?") - @Type(SettingType.Boolean) - public boolean generateImage = false; + @Comment("Send the new password drawn in an image?") + public static final Property PASSWORD_AS_IMAGE = + newProperty(BOOLEAN, "Email.generateImage", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "emails.yml"); + @Comment("The OAuth2 token") + public static final Property OAUTH2_TOKEN = + newProperty(STRING, "Email.emailOauth2Token", ""); - private EmailSettings instance; + private EmailSettings() { + } - public EmailSettings() - { - super(configFile); - instance = this; - if (this.isFirstLaunch) - { - this.emailBlackListed.add("10minutemail.com"); - save(); - } - } - - public EmailSettings getInstance() { - return instance; - } - - public void setInstance(EmailSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java index 307c79062..6f75eb247 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -1,54 +1,39 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.PropertyType; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; -public class HooksSettings extends CustomSetting { +public class HooksSettings implements SettingsClass { @Comment("Do we need to hook with multiverse for spawn checking?") - @Type(SettingType.Boolean) - public boolean multiverse = true; + public static final Property MULTIVERSE = + newProperty(PropertyType.BOOLEAN, "Hooks.multiverse", true); - @Comment("Do we need to hook with BungeeCord ?") - @Type(SettingType.Boolean) - public boolean bungeecord = false; + @Comment("Do we need to hook with BungeeCord?") + public static final Property BUNGEECORD = + newProperty(PropertyType.BOOLEAN, "Hooks.bungeecord", false); @Comment("Send player to this BungeeCord server after register/login") - @Type(SettingType.String) - public String sendPlayerTo = ""; + public static final Property BUNGEECORD_SERVER = + newProperty(PropertyType.STRING, "bungeecord.server", ""); @Comment("Do we need to disable Essentials SocialSpy on join?") - @Type(SettingType.Boolean) - public boolean disableSocialSpy = false; + public static final Property DISABLE_SOCIAL_SPY = + newProperty(PropertyType.BOOLEAN, "Hooks.disableSocialSpy", false); @Comment("Do we need to force /motd Essentials command on join?") - @Type(SettingType.Boolean) - public boolean useEssentialsMotd = false; + public static final Property USE_ESSENTIALS_MOTD = + newProperty(PropertyType.BOOLEAN, "Hooks.useEssentialsMotd", false); @Comment("Do we need to cache custom Attributes?") - @Type(SettingType.Boolean) - public boolean customAttributes = false; + public static final Property CACHE_CUSTOM_ATTRIBUTES = + newProperty(PropertyType.BOOLEAN, "Hooks.customAttributes", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "hooks.yml"); + private HooksSettings() { + } - private HooksSettings instance; - - public HooksSettings() - { - super(configFile); - instance = this; - } - - public HooksSettings getInstance() { - return instance; - } - - public void setInstance(HooksSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java new file mode 100644 index 000000000..b64d2f2ec --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -0,0 +1,148 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; +import fr.xephi.authme.settings.custom.propertymap.PropertyMap; +import fr.xephi.authme.util.CollectionUtils; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * The new settings manager. + */ +public class NewSetting { + + private static final List> CONFIGURATION_CLASSES = Arrays.asList( + ConverterSettings.class, DatabaseSettings.class, EmailSettings.class, HooksSettings.class, + ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class); + + private static final int YAML_INDENTATION = 4; + + private File file; + private YamlConfiguration configuration; + + public NewSetting(File file) { + this.configuration = YamlConfiguration.loadConfiguration(file); + this.file = file; + } + + // TODO: No way of passing just a YamlConfiguration object (later on for mocking purposes) ? + // If not, best is probably to keep this constructor as package-private with @VisibleForTesting + // but it's not a satisfying solution + public NewSetting(YamlConfiguration yamlConfiguration, String file) { + this.configuration = yamlConfiguration; + this.file = new File(file); + } + + public T getOption(Property property) { + return property.getFromFile(configuration); + } + + public void save() { + PropertyMap properties = getAllPropertyFields(); + + try (FileWriter writer = new FileWriter(file)) { + writer.write(""); + + // Contains all but the last node of the setting, e.g. [DataSource, mysql] for "DataSource.mysql.username" + List currentPath = new ArrayList<>(); + for (Map.Entry entry : properties.entrySet()) { + Property property = entry.getKey(); + + // Handle properties + List propertyPath = Arrays.asList(property.getPath().split("\\.")); + List commonPathParts = CollectionUtils.filterCommonStart( + currentPath, propertyPath.subList(0, propertyPath.size() - 1)); + List newPathParts = CollectionUtils.getRange(propertyPath, commonPathParts.size()); + + if (commonPathParts.isEmpty()) { + writer.append("\n"); + } + + int indentationLevel = commonPathParts.size(); + if (newPathParts.size() > 1) { + for (String path : newPathParts.subList(0, newPathParts.size() - 1)) { + writer.append("\n") + .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(path) + .append(": "); + ++indentationLevel; + } + } + for (String comment : entry.getValue()) { + writer.append("\n") + .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append("# ") + .append(comment); + } + writer.append("\n") + .append(StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION)) + .append(CollectionUtils.getRange(newPathParts, newPathParts.size() - 1).get(0)) + .append(": "); + + List yamlLines = property.formatValueAsYaml(configuration); + String delim = ""; + for (String yamlLine : yamlLines) { + writer.append(delim).append(yamlLine); + delim = "\n" + StringUtils.repeat(" ", indentationLevel * YAML_INDENTATION); + } + + currentPath = propertyPath.subList(0, propertyPath.size() - 1); + } + writer.flush(); + writer.close(); + } catch (IOException e) { + ConsoleLogger.showError("Could not save config file - " + StringUtils.formatException(e)); + ConsoleLogger.writeStackTrace(e); + } + } + + private static PropertyMap getAllPropertyFields() { + PropertyMap properties = new PropertyMap(); + for (Class clazz : CONFIGURATION_CLASSES) { + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + Property property = getFieldIfRelevant(field); + if (property != null) { + properties.put(property, getCommentsForField(field)); + } + } + } + return properties; + } + + private static String[] getCommentsForField(Field field) { + if (field.isAnnotationPresent(Comment.class)) { + return field.getAnnotation(Comment.class).value(); + } + return new String[0]; + } + + private static Property getFieldIfRelevant(Field field) { + field.setAccessible(true); + if (field.isAccessible() && Property.class.equals(field.getType()) && Modifier.isStatic(field.getModifiers())) { + try { + return (Property) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not fetch field '" + field.getName() + "' from class '" + + field.getDeclaringClass().getSimpleName() + "': " + StringUtils.formatException(e)); + } + } + return null; + } + + + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java index e8e2d059c..9f5daf952 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java @@ -1,63 +1,46 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; + import java.util.List; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING_LIST; -public class ProtectionSettings extends CustomSetting { - @Comment("Enable some servers protection ( country based login, antibot )") - @Type(SettingType.Boolean) - public boolean enableProtection = false; +public class ProtectionSettings implements SettingsClass { + + @Comment("Enable some servers protection (country based login, antibot)") + public static final Property ENABLE_PROTECTION = + newProperty(BOOLEAN, "Protection.enableProtection", false); @Comment({"Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes", - "PLEASE USE QUOTES !"}) - @Type(SettingType.StringList) - public List countriesWhitelist = new ArrayList(); + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_WHITELIST = + newProperty(STRING_LIST, "Protection.countries", "US", "GB", "A1"); @Comment({"Countries not allowed to join the server and register", - "PLEASE USE QUOTES !"}) - @Type(SettingType.StringList) - public List countriesBlacklist = new ArrayList(); + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_BLACKLIST = + newProperty(STRING_LIST, "Protection.countriesBlacklist"); @Comment("Do we need to enable automatic antibot system?") - @Type(SettingType.Boolean) - public boolean enableAntiBot = false; + public static final Property ENABLE_ANTIBOT = + newProperty(BOOLEAN, "Protection.enableAntiBot", false); @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") - @Type(SettingType.Int) - public int antiBotSensibility = 5; + public static final Property ANTIBOT_SENSIBILITY = + newProperty(INTEGER, "Protection.antiBotSensibility", 5); @Comment("Duration in minutes of the antibot automatic system") - @Type(SettingType.Int) - public int antiBotDuration = 10; + public static final Property ANTIBOT_DURATION = + newProperty(INTEGER, "Protection.antiBotDuration", 10); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "protection.yml"); + private ProtectionSettings() { + } - private ProtectionSettings instance; - - public ProtectionSettings() - { - super(configFile); - instance = this; - if (this.isFirstLaunch) - { - this.countriesWhitelist.add("US"); - this.countriesWhitelist.add("GB"); - this.countriesBlacklist.add("A1"); - save(); - } - } - - public ProtectionSettings getInstance() { - return instance; - } - - public void setInstance(ProtectionSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java index 3b85e2fb0..c46e86482 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -1,62 +1,49 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING; -public class PurgeSettings extends CustomSetting { +public class PurgeSettings implements SettingsClass { @Comment("If enabled, AuthMe automatically purges old, unused accounts") - @Type(SettingType.Boolean) - public boolean useAutoPurge = false; + public static final Property USE_AUTO_PURGE = + newProperty(BOOLEAN, "Purge.useAutoPurge", false); @Comment("Number of Days an account become Unused") - @Type(SettingType.Int) - public int daysBeforeRemovePlayer = 60; + public static final Property DAYS_BEFORE_REMOVE_PLAYER = + newProperty(INTEGER, "Purge.daysBeforeRemovePlayer", 60); @Comment("Do we need to remove the player.dat file during purge process?") - @Type(SettingType.Boolean) - public boolean removePlayerDat = false; + public static final Property REMOVE_PLAYER_DAT = + newProperty(BOOLEAN, "Purge.removePlayerDat", false); @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") - @Type(SettingType.Boolean) - public boolean removeEssentialsFiles = false; + public static final Property REMOVE_ESSENTIALS_FILES = + newProperty(BOOLEAN, "Purge.removeEssentialsFiles", false); @Comment("World where are players.dat stores") - @Type(SettingType.String) - public String defaultWorld = "world"; + public static final Property DEFAULT_WORLD = + newProperty(STRING, "Purge.defaultWorld", "world"); @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") - @Type(SettingType.Boolean) - public boolean removeLimiteCreativeInventories = false; + public static final Property REMOVE_LIMITED_CREATIVE_INVENTORIES = + newProperty(BOOLEAN, "Purge.removeLimitedCreativesInventories", false); @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") - @Type(SettingType.Boolean) - public boolean removeAntiXRayFile = false; + public static final Property REMOVE_ANTI_XRAY_FILE = + newProperty(BOOLEAN, "Purge.removeAntiXRayFile", false); @Comment("Do we need to remove permissions?") - @Type(SettingType.Boolean) - public boolean removePermissions = false; + public static final Property REMOVE_PERMISSIONS = + newProperty(BOOLEAN, "Purge.removePermissions", false); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "purge.yml"); + private PurgeSettings() { + } - private PurgeSettings instance; - - public PurgeSettings() - { - super(configFile); - instance = this; - } - - public PurgeSettings getInstance() { - return instance; - } - - public void setInstance(PurgeSettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java index 3166cbe26..4b4638b3e 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -1,60 +1,51 @@ package fr.xephi.authme.settings.custom; -import java.io.File; -import java.util.ArrayList; -import java.util.List; +import fr.xephi.authme.settings.custom.domain.Comment; +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.SettingsClass; -import fr.xephi.authme.settings.custom.annotations.Comment; -import fr.xephi.authme.settings.custom.annotations.Type; -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.custom.domain.PropertyType.INTEGER; -public class SecuritySettings extends CustomSetting { +public class SecuritySettings implements SettingsClass { @Comment({"Stop the server if we can't contact the sql database", - "Take care with this, if you set that to false,", - "AuthMe automatically disable and the server is not protected!"}) - @Type(SettingType.Boolean) - public boolean stopServerOnProblem = true; + "Take care with this, if you set this to false,", + "AuthMe will automatically disable and the server won't be protected!"}) + public static final Property STOP_SERVER_ON_PROBLEM = + newProperty(BOOLEAN, "Security.SQLProblem.stopServer", true); @Comment("/reload support") - @Type(SettingType.Boolean) - public boolean useReloadCommandSupport = true; + public static final Property USE_RELOAD_COMMAND_SUPPORT = + newProperty(BOOLEAN, "Security.ReloadCommand.useReloadCommandSupport", true); - @Comment("Remove Spam from Console ?") - @Type(SettingType.Boolean) - public boolean removeSpamFromConsole = false; + @Comment("Remove spam from console?") + public static final Property REMOVE_SPAM_FROM_CONSOLE = + newProperty(BOOLEAN, "Security.console.noConsoleSpam", false); - @Comment("Remove Password from Console ?") - @Type(SettingType.Boolean) - public boolean removePasswordFromConsole = true; + @Comment("Remove passwords from console?") + public static final Property REMOVE_PASSWORD_FROM_CONSOLE = + newProperty(BOOLEAN, "Security.console.removePassword", true); @Comment("Player need to put a captcha when he fails too lot the password") - @Type(SettingType.Boolean) - public boolean useCaptcha = false; + public static final Property USE_CAPTCHA = + newProperty(BOOLEAN, "Security.captcha.useCaptcha", false); @Comment("Max allowed tries before request a captcha") - @Type(SettingType.Int) - public int maxLoginTryBeforeCaptcha = 5; + public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = + newProperty(INTEGER, "Security.captcha.maxLoginTry", 5); - @Comment("Captcha length ") - @Type(SettingType.Int) - public int captchaLength = 5; + @Comment("Captcha length") + public static final Property CAPTCHA_LENGTH = + newProperty(INTEGER, "Security.captcha.captchaLength", 5); - private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "security.yml"); + @Comment({"Kick players before stopping the server, that allow us to save position of players", + "and all needed information correctly without any corruption."}) + public static final Property KICK_PLAYERS_BEFORE_STOPPING = + newProperty(BOOLEAN, "Security.stop.kickPlayersBeforeStopping", true); - private SecuritySettings instance; + private SecuritySettings() { + } - public SecuritySettings() - { - super(configFile); - instance = this; - } - - public SecuritySettings getInstance() { - return instance; - } - - public void setInstance(SecuritySettings instance) { - this.instance = instance; - } } diff --git a/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java b/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java deleted file mode 100644 index aae0af52b..000000000 --- a/src/main/java/fr/xephi/authme/settings/custom/annotations/Type.java +++ /dev/null @@ -1,43 +0,0 @@ -package fr.xephi.authme.settings.custom.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import fr.xephi.authme.settings.custom.annotations.Type.SettingType; - -/** -* -* Set the type of a field value -* -* @author xephi59 -* -*/ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Type { - - public enum SettingType { - String(0), - Int(1), - Boolean(2), - Double(3), - StringList(4), - Long(5); - - private int type; - - SettingType(int type) - { - this.type = type; - } - - public int getType() - { - return this.type; - } - } - - public SettingType value() default SettingType.String; -} diff --git a/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java b/src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java similarity index 62% rename from src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java rename to src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java index d711a1943..f664e424b 100644 --- a/src/main/java/fr/xephi/authme/settings/custom/annotations/Comment.java +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/Comment.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.settings.custom.annotations; +package fr.xephi.authme.settings.custom.domain; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -6,15 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * - * Add a comment to a field value - * - * @author xephi59 - * + * Comment for properties which are also included in the YAML file upon saving. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Comment { - public String[] value() default ""; + String[] value() default ""; + } diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java b/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java new file mode 100644 index 000000000..e777f6ec9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/Property.java @@ -0,0 +1,100 @@ +package fr.xephi.authme.settings.custom.domain; + +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Properties (i.e. a setting that is read from the config.yml file). + */ +public class Property { + + private final PropertyType type; + private final String path; + private final T defaultValue; + + private Property(PropertyType type, String path, T defaultValue) { + Objects.requireNonNull(defaultValue); + this.type = type; + this.path = path; + this.defaultValue = defaultValue; + } + + public static Property newProperty(PropertyType type, String path, T defaultValue) { + return new Property<>(type, path, defaultValue); + } + + @SafeVarargs + public static Property> newProperty(PropertyType> type, String path, U... defaultValues) { + return new Property<>(type, path, Arrays.asList(defaultValues)); + } + + // ----- + // Overloaded convenience methods for specific types + // ----- + public static Property newProperty(String path, boolean defaultValue) { + return new Property<>(PropertyType.BOOLEAN, path, defaultValue); + } + + public static Property newProperty(String path, int defaultValue) { + return new Property<>(PropertyType.INTEGER, path, defaultValue); + } + + public static Property newProperty(String path, String defaultValue) { + return new Property<>(PropertyType.STRING, path, defaultValue); + } + + // ----- + // Hooks to the PropertyType methods + // ----- + + /** + * Get the property value from the given configuration. + * + * @param configuration The configuration to read the value from + * @return The value, or default if not present + */ + public T getFromFile(YamlConfiguration configuration) { + return type.getFromFile(this, configuration); + } + + /** + * Format the property value as YAML. + * + * @param configuration The configuration to read the value from + * @return The property value as YAML + */ + public List formatValueAsYaml(YamlConfiguration configuration) { + return type.asYaml(this, configuration); + } + + // ----- + // Trivial getters + // ----- + + /** + * Return the default value of the property. + * + * @return The default value + */ + public T getDefaultValue() { + return defaultValue; + } + + /** + * Return the property path (i.e. the node at which this property is located in the YAML file). + * + * @return The path + */ + public String getPath() { + return path; + } + + @Override + public String toString() { + return "Property '" + path + "'"; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java new file mode 100644 index 000000000..3680de3f9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/PropertyType.java @@ -0,0 +1,146 @@ +package fr.xephi.authme.settings.custom.domain; + +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; + +/** + * Handles a certain property type and provides type-specific functionality. + * + * @param The value of the property + * @see Property + */ +public abstract class PropertyType { + + public static final PropertyType BOOLEAN = new BooleanProperty(); + public static final PropertyType DOUBLE = new DoubleProperty(); + public static final PropertyType INTEGER = new IntegerProperty(); + public static final PropertyType STRING = new StringProperty(); + public static final PropertyType> STRING_LIST = new StringListProperty(); + + /** + * Get the property's value from the given YAML configuration. + * + * @param property The property to retrieve + * @param configuration The YAML configuration to read from + * @return The read value, or the default value if absent + */ + public abstract T getFromFile(Property property, YamlConfiguration configuration); + + /** + * Return the property's value (or its default) as YAML. + * + * @param property The property to transform + * @param configuration The YAML configuration to read from + * @return The read value or its default in YAML format + */ + public List asYaml(Property property, YamlConfiguration configuration) { + return asYaml(getFromFile(property, configuration)); + } + + /** + * Transform the given value to YAML. + * + * @param value The value to transform + * @return The value as YAML + */ + protected abstract List asYaml(T value); + + + /** + * Boolean property. + */ + private static final class BooleanProperty extends PropertyType { + @Override + public Boolean getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getBoolean(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Boolean value) { + return asList(value ? "true" : "false"); + } + } + + /** + * Double property. + */ + private static final class DoubleProperty extends PropertyType { + @Override + public Double getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getDouble(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Double value) { + return asList(String.valueOf(value)); + } + } + + /** + * Integer property. + */ + private static final class IntegerProperty extends PropertyType { + @Override + public Integer getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getInt(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Integer value) { + return asList(String.valueOf(value)); + } + } + + /** + * String property. + */ + private static final class StringProperty extends PropertyType { + @Override + public String getFromFile(Property property, YamlConfiguration configuration) { + return configuration.getString(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(String value) { + return asList(toYamlLiteral(value)); + } + + public static String toYamlLiteral(String str) { + // TODO: Need to handle new lines properly + return "'" + str.replace("'", "''") + "'"; + } + } + + /** + * String list property. + */ + private static final class StringListProperty extends PropertyType> { + @Override + public List getFromFile(Property> property, YamlConfiguration configuration) { + if (!configuration.isList(property.getPath())) { + return property.getDefaultValue(); + } + return configuration.getStringList(property.getPath()); + } + + @Override + protected List asYaml(List value) { + if (value.isEmpty()) { + return asList("[]"); + } + + List resultLines = new ArrayList<>(); + resultLines.add(""); // add + for (String entry : value) { + // TODO: StringProperty#toYamlLiteral will return List... + resultLines.add(" - " + StringProperty.toYamlLiteral(entry)); + } + return resultLines; + } + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java b/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java new file mode 100644 index 000000000..6820e12e1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/domain/SettingsClass.java @@ -0,0 +1,7 @@ +package fr.xephi.authme.settings.custom.domain; + +/** + * Marker for classes that define {@link Property} fields. + */ +public interface SettingsClass { +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java b/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java new file mode 100644 index 000000000..4e621617e --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/propertymap/Node.java @@ -0,0 +1,121 @@ +package fr.xephi.authme.settings.custom.propertymap; + +import java.util.ArrayList; +import java.util.List; + +/** + * Node class for building a tree from supplied String paths, ordered by insertion. + *

+ * For instance, consider a tree to which the following paths are inserted (in the given order): + * "animal.bird.duck", "color.yellow", "animal.rodent.rat", "animal.rodent.rabbit", "color.red". + * For such a tree:

    + *
  • "animal" (or any of its children) is sorted before "color" (or any of its children)
  • + *
  • "animal.bird" or any child thereof is sorted before "animal.rodent"
  • + *
  • "animal.rodent.rat" comes before "animal.rodent.rabbit"
  • + *
+ * + * @see PropertyMapComparator + */ +final class Node { + + private final String name; + private final List children; + + private Node(String name) { + this.name = name; + this.children = new ArrayList<>(); + } + + /** + * Create a root node, i.e. the starting node for a new tree. Call this method to create + * a new tree and always pass this root to other methods. + * + * @return The generated root node. + */ + public static Node createRoot() { + return new Node(null); + } + + /** + * Add a node to the root, creating any intermediary children that don't exist. + * + * @param root The root to add the path to + * @param fullPath The entire path of the node to add, separate by periods + */ + public static void addNode(Node root, String fullPath) { + String[] pathParts = fullPath.split("\\."); + Node parent = root; + for (String part : pathParts) { + Node child = parent.getChild(part); + if (child == null) { + child = new Node(part); + parent.children.add(child); + } + parent = child; + } + } + + /** + * Compare two nodes by this class' sorting behavior (insertion order). + * Note that this method assumes that both supplied paths exist in the tree. + * + * @param root The root of the tree + * @param fullPath1 The full path to the first node + * @param fullPath2 The full path to the second node + * @return The comparison result, in the same format as {@link Comparable#compareTo} + */ + public static int compare(Node root, String fullPath1, String fullPath2) { + String[] path1 = fullPath1.split("\\."); + String[] path2 = fullPath2.split("\\."); + + int commonCount = 0; + Node commonNode = root; + while (commonCount < path1.length && commonCount < path2.length + && path1[commonCount].equals(path2[commonCount]) && commonNode != null) { + commonNode = commonNode.getChild(path1[commonCount]); + ++commonCount; + } + + if (commonNode == null) { + System.err.println("Could not find common node for '" + fullPath1 + "' at index " + commonCount); + return fullPath1.compareTo(fullPath2); // fallback + } else if (commonCount >= path1.length || commonCount >= path2.length) { + return Integer.compare(path1.length, path2.length); + } + int child1Index = commonNode.getChildIndex(path1[commonCount]); + int child2Index = commonNode.getChildIndex(path2[commonCount]); + return Integer.compare(child1Index, child2Index); + } + + private Node getChild(String name) { + for (Node child : children) { + if (child.name.equals(name)) { + return child; + } + } + return null; + } + + /** + * Return the child's index, i.e. the position at which it was inserted to its parent. + * + * @param name The name of the node + * @return The insertion index + */ + private int getChildIndex(String name) { + int i = 0; + for (Node child : children) { + if (child.name.equals(name)) { + return i; + } + ++i; + } + return -1; + } + + @Override + public String toString() { + return "Node '" + name + "'"; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java new file mode 100644 index 000000000..e539db7ab --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMap.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.settings.custom.propertymap; + +import fr.xephi.authme.settings.custom.domain.Property; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Class wrapping a {@code Map} for storing properties and their associated + * comments with custom ordering. + * + * @see PropertyMapComparator for details about the map's order + */ +public class PropertyMap { + + private Map propertyMap; + private PropertyMapComparator comparator; + + /** + * Create a new property map. + */ + public PropertyMap() { + comparator = new PropertyMapComparator(); + propertyMap = new TreeMap<>(comparator); + } + + /** + * Add a new property to the map. + * + * @param property The property to add + * @param comments The comments associated to the property + */ + public void put(Property property, String[] comments) { + comparator.add(property); + propertyMap.put(property, comments); + } + + /** + * Return the entry set of the map. + * + * @return The entry set + */ + public Set> entrySet() { + return propertyMap.entrySet(); + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java new file mode 100644 index 000000000..9b07070bc --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/propertymap/PropertyMapComparator.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.settings.custom.propertymap; + +import fr.xephi.authme.settings.custom.domain.Property; + +import java.util.Comparator; + +/** + * Custom comparator for {@link PropertyMap}. It guarantees that the map's entries: + *
    + *
  • are grouped by path, e.g. all "DataSource.mysql" properties are together, and "DataSource.mysql" properties + * are within the broader "DataSource" group.
  • + *
  • are ordered by insertion, e.g. if the first "DataSource" property is inserted before the first "security" + * property, then "DataSource" properties will come before the "security" ones.
  • + *
+ */ +final class PropertyMapComparator implements Comparator { + + private Node parent = Node.createRoot(); + + /** + * Method to call when adding a new property to the map (as to retain its insertion time). + * + * @param property The property that is being added + */ + public void add(Property property) { + Node.addNode(parent, property.getPath()); + } + + @Override + public int compare(Property p1, Property p2) { + return Node.compare(parent, p1.getPath(), p2.getPath()); + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + +} diff --git a/src/main/java/fr/xephi/authme/util/CollectionUtils.java b/src/main/java/fr/xephi/authme/util/CollectionUtils.java index 5c3eaa7f7..519951655 100644 --- a/src/main/java/fr/xephi/authme/util/CollectionUtils.java +++ b/src/main/java/fr/xephi/authme/util/CollectionUtils.java @@ -3,6 +3,7 @@ package fr.xephi.authme.util; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; /** * Utils class for collections. @@ -58,4 +59,17 @@ public final class CollectionUtils { public static boolean isEmpty(Collection coll) { return coll == null || coll.isEmpty(); } + + public static List filterCommonStart(List coll1, List coll2) { + List commonStart = new ArrayList<>(); + int minSize = Math.min(coll1.size(), coll2.size()); + for (int i = 0; i < minSize; ++i) { + if (Objects.equals(coll1.get(i), coll2.get(i))) { + commonStart.add(coll1.get(i)); + } else { + break; + } + } + return commonStart; + } } diff --git a/src/main/java/fr/xephi/authme/util/StringUtils.java b/src/main/java/fr/xephi/authme/util/StringUtils.java index 9ae0ec0fd..2ee243819 100644 --- a/src/main/java/fr/xephi/authme/util/StringUtils.java +++ b/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -117,4 +117,12 @@ public final class StringUtils { return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); } + public static String repeat(String str, int times) { + StringBuilder sb = new StringBuilder(str.length() * times); + for (int i = 0; i < times; ++i) { + sb.append(str); + } + return sb.toString(); + } + } diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java new file mode 100644 index 000000000..39a5062c8 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -0,0 +1,111 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.custom.domain.Property; +import fr.xephi.authme.settings.custom.domain.PropertyType; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.File; +import java.net.URL; +import java.util.List; + +import static fr.xephi.authme.settings.custom.domain.Property.newProperty; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; + +public class NewSettingTest { + + private static final String CONFIG_FILE = "437-config-test.yml"; + + @Test + public void shouldReturnIntegerFromFile() { + // given + YamlConfiguration file = mock(YamlConfiguration.class); + Property config = TestConfiguration.DURATION_IN_SECONDS; + given(file.getInt("test.duration", 4)).willReturn(18); + NewSetting settings = new NewSetting(file, "conf.txt"); + + // when + int retrieve = settings.getOption(config); + + // then + assertThat(retrieve, equalTo(18)); + } + + @Test + public void shouldLoadAllConfigs() { + // given + YamlConfiguration file = mock(YamlConfiguration.class); + + given(file.getString(anyString(), anyString())).willAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + // Return the second parameter -> the default + return (String) invocation.getArguments()[1]; + } + }); + + given(file.getInt(eq(EmailSettings.RECOVERY_PASSWORD_LENGTH.getPath()), anyInt())) + .willReturn(20); + given(file.getBoolean(eq(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE.getPath()), anyBoolean())) + .willReturn(false); + + // when + NewSetting settings = new NewSetting(file, "conf.txt"); + + // then + // Expect the value we told the YAML mock to return: + assertThat(settings.getOption(EmailSettings.RECOVERY_PASSWORD_LENGTH), equalTo(20)); + // Expect the default: + assertThat(settings.getOption(EmailSettings.SMTP_HOST), equalTo(EmailSettings.SMTP_HOST.getDefaultValue())); + // Expect the value we told the YAML mock to return: + assertThat(settings.getOption(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE), equalTo(false)); + } + + @Test + public void executeIntegrationTest() { + // given + YamlConfiguration yamlFile = YamlConfiguration.loadConfiguration(getConfigFile()); + NewSetting settings = new NewSetting(yamlFile, "conf.txt"); + + // when + int result = settings.getOption(TestConfiguration.DURATION_IN_SECONDS); + String systemName = settings.getOption(TestConfiguration.SYSTEM_NAME); + String helpHeader = settings.getOption(newProperty("settings.helpHeader", "")); + List unsafePasswords = settings.getOption( + newProperty(PropertyType.STRING_LIST, "Security.unsafePasswords")); + + // then + assertThat(result, equalTo(22)); + assertThat(systemName, equalTo(TestConfiguration.SYSTEM_NAME.getDefaultValue())); + assertThat(helpHeader, equalTo("AuthMeReloaded")); + assertThat(unsafePasswords, contains("123456", "qwerty", "54321")); + } + + private File getConfigFile() { + URL url = getClass().getClassLoader().getResource(CONFIG_FILE); + if (url == null) { + throw new RuntimeException("File '" + CONFIG_FILE + "' could not be loaded"); + } + return new File(url.getFile()); + } + + private static class TestConfiguration { + + public static final Property DURATION_IN_SECONDS = + newProperty("test.duration", 4); + + public static final Property SYSTEM_NAME = + newProperty("test.systemName", "[TestDefaultValue]"); + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java new file mode 100644 index 000000000..1dadf75e6 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingsWriteTest.java @@ -0,0 +1,42 @@ +package fr.xephi.authme.settings.custom; + +import org.junit.Test; + +import java.io.File; +import java.net.URL; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * Test for the save() function of new settings + */ +public class NewSettingsWriteTest { + + private static final String CONFIG_FILE = "437-write-test.yml"; + + @Test + public void shouldWriteProperties() { + File file = getConfigFile(); + NewSetting setting = new NewSetting(file); + setting.save(); + + // assert that we can load the file again -- i.e. that it's valid YAML! + NewSetting newSetting = new NewSetting(file); + assertThat(newSetting.getOption(SecuritySettings.CAPTCHA_LENGTH), + equalTo(SecuritySettings.CAPTCHA_LENGTH.getDefaultValue())); + assertThat(newSetting.getOption(ProtectionSettings.COUNTRIES_BLACKLIST), + equalTo(ProtectionSettings.COUNTRIES_BLACKLIST.getDefaultValue())); + } + + + + private File getConfigFile() { + URL url = getClass().getClassLoader().getResource(CONFIG_FILE); + if (url == null) { + throw new RuntimeException("File '" + CONFIG_FILE + "' could not be loaded"); + } + return new File(url.getFile()); + } + +} diff --git a/src/test/resources/437-config-test.yml b/src/test/resources/437-config-test.yml new file mode 100644 index 000000000..f0d9f11b9 --- /dev/null +++ b/src/test/resources/437-config-test.yml @@ -0,0 +1,44 @@ +test: + duration: 22 +DataSource: + # What type of database do you want to use? + # Valid values: sqlite, mysql + backend: sqlite + # Enable database caching, should improve database performance + caching: true +settings: + # The name shown in the help messages. + helpHeader: AuthMeReloaded + GameMode: + # ForceSurvivalMode to player when join ? + ForceSurvivalMode: false +Security: + SQLProblem: + # Stop the server if we can't contact the sql database + # Take care with this, if you set that to false, + # AuthMe automatically disable and the server is not protected! + stopServer: true + ReloadCommand: + # /reload support + useReloadCommandSupport: true + console: + # Remove spam console + noConsoleSpam: false + # Replace passwords in the console when player type a command like /login + removePassword: true + captcha: + # Player need to put a captcha when he fails too lot the password + useCaptcha: false + # Max allowed tries before request a captcha + maxLoginTry: 5 + # Captcha length + captchaLength: 5 + unsafePasswords: + - '123456' + - 'qwerty' + - '54321' +Email: + # Email SMTP server host + mailSMTP: smtp.gmail.com + # Email SMTP server port + mailPort: 465 diff --git a/src/test/resources/437-write-test.yml b/src/test/resources/437-write-test.yml new file mode 100644 index 000000000..de2b762f8 --- /dev/null +++ b/src/test/resources/437-write-test.yml @@ -0,0 +1,172 @@ + + +Converter: + Rakamak: + # Rakamak file name + fileName: 'users.rak' + # Rakamak use IP? + useIP: false + # Rakamak IP file name + ipFileName: 'UsersIp.rak' + CrazyLogin: + # CrazyLogin database file name + fileName: 'accounts.db' + +DataSource: + # What type of database do you want to use? + # Valid values: sqlite, mysql + backend: 'sqlite' + # Enable database caching, should improve database performance + caching: true + # Database host address + mySQLHost: '127.0.0.1' + # Database port + mySQLPort: '3306' + # Username about Database Connection Infos + mySQLUsername: 'authme' + # Password about Database Connection Infos + mySQLPassword: '123456' + # Database Name, use with converters or as SQLITE database name + mySQLDatabase: 'authme' + # Table of the database + mySQLTablename: 'authme' + # Column of IDs to sort data + mySQLColumnId: 'id' + # Column for storing or checking players nickname + mySQLColumnName: 'username' + # Column for storing or checking players RealName + mySQLRealName: 'realname' + # Column for storing players passwords + mySQLColumnPassword: 'password' + # Column for storing players emails + mySQLColumnEmail: 'email' + # Column for storing if a player is logged in or not + mySQLColumnLogged: 'email' + # Column for storing players ips + mySQLColumnIp: 'ip' + # Column for storing players lastlogins + mySQLColumnLastLogin: 'lastlogin' + # Column for storing player LastLocation - X + mySQLlastlocX: 'x' + # Column for storing player LastLocation - Y + mySQLlastlocY: 'y' + # Column for storing player LastLocation - Z + mySQLlastlocZ: 'z' + # Column for storing player LastLocation - World Name + mySQLlastlocWorld: 'world' + # Enable this when you allow registration through a website + mySQLWebsite: false + +ExternalBoardOptions: + # Column for storing players passwords salts + mySQLColumnSalt: '' + # Column for storing players groups + mySQLColumnGroup: '' + +Email: + # Email SMTP server host + mailSMTP: 'smtp.gmail.com' + # Email SMTP server port + mailPort: 465 + # Email account which sends the mails + mailAccount: '' + # Email account password + mailPassword: '' + # Recovery password length + RecoveryPasswordLength: 8 + # Mail Subject + mailSubject: 'Your new AuthMe password' + # Like maxRegPerIP but with email + maxRegPerEmail: 1 + # Recall players to add an email? + recallPlayers: false + # Delay in minute for the recall scheduler + delayRecall: 5 + # Blacklist these domains for emails + emailBlacklisted: + - '10minutemail.com' + # Whitelist ONLY these domains for emails + emailWhitelisted: [] + # Send the new password drawn in an image? + generateImage: false + # The OAuth2 token + emailOauth2Token: '' + +Hooks: + # Do we need to hook with multiverse for spawn checking? + multiverse: true + # Do we need to hook with BungeeCord? + bungeecord: false + # Do we need to disable Essentials SocialSpy on join? + disableSocialSpy: false + # Do we need to force /motd Essentials command on join? + useEssentialsMotd: false + # Do we need to cache custom Attributes? + customAttributes: false + +bungeecord: + # Send player to this BungeeCord server after register/login + server: '' + +Protection: + # Enable some servers protection (country based login, antibot) + enableProtection: false + # Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes + # PLEASE USE QUOTES! + countries: + - 'US' + - 'GB' + - 'A1' + # Countries not allowed to join the server and register + # PLEASE USE QUOTES! + countriesBlacklist: [] + # Do we need to enable automatic antibot system? + enableAntiBot: false + # Max number of player allowed to login in 5 secs before enable AntiBot system automatically + antiBotSensibility: 5 + # Duration in minutes of the antibot automatic system + antiBotDuration: 10 + +Purge: + # If enabled, AuthMe automatically purges old, unused accounts + useAutoPurge: false + # Number of Days an account become Unused + daysBeforeRemovePlayer: 60 + # Do we need to remove the player.dat file during purge process? + removePlayerDat: false + # Do we need to remove the Essentials/users/player.yml file during purge process? + removeEssentialsFiles: false + # World where are players.dat stores + defaultWorld: 'world' + # Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ? + removeLimitedCreativesInventories: false + # Do we need to remove the AntiXRayData/PlayerData/player file during purge process? + removeAntiXRayFile: false + # Do we need to remove permissions? + removePermissions: false + +Security: + SQLProblem: + # Stop the server if we can't contact the sql database + # Take care with this, if you set this to false, + # AuthMe will automatically disable and the server won't be protected! + stopServer: true + ReloadCommand: + # /reload support + useReloadCommandSupport: true + console: + # Remove spam from console? + noConsoleSpam: false + # Remove passwords from console? + removePassword: true + captcha: + # Player need to put a captcha when he fails too lot the password + useCaptcha: false + # Max allowed tries before request a captcha + maxLoginTry: 5 + # Captcha length + captchaLength: 5 + stop: + # Kick players before stopping the server, that allow us to save position of players + # and all needed information correctly without any corruption. + kickPlayersBeforeStopping: true