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
This commit is contained in:
ljacqu 2016-01-03 15:22:32 +01:00
parent c2deb9d0b5
commit 7d41ccbc9c
23 changed files with 1225 additions and 518 deletions

View File

@ -1,46 +1,32 @@
package fr.xephi.authme.settings.custom; package fr.xephi.authme.settings.custom;
import java.io.File; import fr.xephi.authme.settings.custom.domain.Comment;
import java.util.ArrayList; import fr.xephi.authme.settings.custom.domain.Property;
import java.util.List; import fr.xephi.authme.settings.custom.domain.SettingsClass;
import fr.xephi.authme.settings.custom.annotations.Comment; import static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type; import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType; import static fr.xephi.authme.settings.custom.domain.PropertyType.STRING;
public class ConverterSettings extends CustomSetting { public class ConverterSettings implements SettingsClass {
@Comment("Rakamak file name") @Comment("Rakamak file name")
@Type(SettingType.String) public static final Property<String> RAKAMAK_FILE_NAME =
public String rakamakFileName = "users.rak"; newProperty(STRING, "Converter.Rakamak.fileName", "users.rak");
@Comment("Rakamak use Ip ?") @Comment("Rakamak use IP?")
@Type(SettingType.Boolean) public static final Property<Boolean> RAKAMAK_USE_IP =
public boolean rakamakeUseIP = false; newProperty(BOOLEAN, "Converter.Rakamak.useIP", false);
@Comment("Rakamak IP file name") @Comment("Rakamak IP file name")
@Type(SettingType.String) public static final Property<String> RAKAMAK_IP_FILE_NAME =
public String rakamakIPFileName = "UsersIp.rak"; newProperty(STRING, "Converter.Rakamak.ipFileName", "UsersIp.rak");
@Comment("CrazyLogin database file name") @Comment("CrazyLogin database file name")
@Type(SettingType.String) public static final Property<String> CRAZYLOGIN_FILE_NAME =
public String crazyLoginFileName = "accounts.db"; 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;
}
} }

View File

@ -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<String> list = (List<String>) 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);
}
}
}

View File

@ -1,121 +1,109 @@
package fr.xephi.authme.settings.custom; 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 static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type; import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType; 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?", @Comment({"What type of database do you want to use?",
"Valid values: sqlite, mysql"}) "Valid values: sqlite, mysql"})
@Type(SettingType.String) public static final Property<String> BACKEND =
public String backend = "sqlite"; newProperty(STRING, "DataSource.backend", "sqlite");
@Comment("Enable database caching, should improve database performance") @Comment("Enable database caching, should improve database performance")
@Type(SettingType.Boolean) public static final Property<Boolean> USE_CACHING =
public boolean caching = true; newProperty(BOOLEAN, "DataSource.caching", true);
@Comment("Database host address") @Comment("Database host address")
@Type(SettingType.String) public static final Property<String> MYSQL_HOST =
public String mySQLHost = "127.0.0.1"; newProperty(STRING, "DataSource.mySQLHost", "127.0.0.1");
@Comment("Database port") @Comment("Database port")
@Type(SettingType.String) public static final Property<String> MYSQL_PORT =
public String mySQLPort = "3306"; newProperty(STRING, "DataSource.mySQLPort", "3306");
@Comment("Username about Database Connection Infos") @Comment("Username about Database Connection Infos")
@Type(SettingType.String) public static final Property<String> MYSQL_USERNAME =
public String mySQLUsername = "authme"; newProperty(STRING, "DataSource.mySQLUsername", "authme");
@Comment("Password about Database Connection Infos") @Comment("Password about Database Connection Infos")
@Type(SettingType.String) public static final Property<String> MYSQL_PASSWORD =
public String mySQLPassword = "12345"; newProperty(STRING, "DataSource.mySQLPassword", "123456");
@Comment("Database Name, use with converters or as SQLITE database name") @Comment("Database Name, use with converters or as SQLITE database name")
@Type(SettingType.String) public static final Property<String> MYSQL_DATABASE =
public String mySQLDatabase = "authme"; newProperty(STRING, "DataSource.mySQLDatabase", "authme");
@Comment("Table of the database") @Comment("Table of the database")
@Type(SettingType.String) public static final Property<String> MYSQL_TABLE =
public String mySQLTablename = "authme"; newProperty(STRING, "DataSource.mySQLTablename", "authme");
@Comment("Column of IDs to sort data") @Comment("Column of IDs to sort data")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_ID =
public String mySQLColumnId = "id"; newProperty(STRING, "DataSource.mySQLColumnId", "id");
@Comment("Column for storing or checking players nickname") @Comment("Column for storing or checking players nickname")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_NAME =
public String mySQLColumnName = "username"; newProperty(STRING, "DataSource.mySQLColumnName", "username");
@Comment("Column for storing or checking players RealName ") @Comment("Column for storing or checking players RealName ")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_REALNAME =
public String mySQLColumnRealName = "realname"; newProperty(STRING, "DataSource.mySQLRealName", "realname");
@Comment("Column for storing players passwords") @Comment("Column for storing players passwords")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_PASSWORD =
public String mySQLColumnPassword = "password"; newProperty(STRING, "DataSource.mySQLColumnPassword", "password");
@Comment("Column for storing players passwords salts") @Comment("Column for storing players passwords salts")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_SALT =
public String mySQLColumnSalt = ""; newProperty(STRING, "ExternalBoardOptions.mySQLColumnSalt", "");
@Comment("Column for storing players emails") @Comment("Column for storing players emails")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_EMAIL =
public String mySQLColumnEmail = "email"; newProperty(STRING, "DataSource.mySQLColumnEmail", "email");
@Comment("Column for storing if a player is logged in or not") @Comment("Column for storing if a player is logged in or not")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_ISLOGGED =
public String mySQLColumnLogged = "isLogged"; newProperty(STRING, "DataSource.mySQLColumnLogged", "isLogged");
@Comment("Column for storing players ips") @Comment("Column for storing players ips")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_IP =
public String mySQLColumnIp = "ip"; newProperty(STRING, "DataSource.mySQLColumnIp", "ip");
@Comment("Column for storing players lastlogins") @Comment("Column for storing players lastlogins")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_LASTLOGIN =
public String mySQLColumnLastLogin = "lastlogin"; newProperty(STRING, "DataSource.mySQLColumnLastLogin", "lastlogin");
@Comment("Column for storing player LastLocation - X") @Comment("Column for storing player LastLocation - X")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_LASTLOC_X =
public String mySQLColumnLastLocX = "x"; newProperty(STRING, "DataSource.mySQLlastlocX", "x");
@Comment("Column for storing player LastLocation - Y") @Comment("Column for storing player LastLocation - Y")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_LASTLOC_Y =
public String mySQLColumnLastLocY = "y"; newProperty(STRING, "DataSource.mySQLlastlocY", "y");
@Comment("Column for storing player LastLocation - Z") @Comment("Column for storing player LastLocation - Z")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_LASTLOC_Z =
public String mySQLColumnLastLocZ = "z"; newProperty(STRING, "DataSource.mySQLlastlocZ", "z");
@Comment("Column for storing player LastLocation - World Name") @Comment("Column for storing player LastLocation - World Name")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_LASTLOC_WORLD =
public String mySQLColumnLastLocWorld = "world"; newProperty(STRING, "DataSource.mySQLlastlocWorld", "world");
@Comment("Column for storing players groups") @Comment("Column for storing players groups")
@Type(SettingType.String) public static final Property<String> MYSQL_COL_GROUP =
public String mySQLColumnGroup = ""; newProperty(STRING, "ExternalBoardOptions.mySQLColumnGroup", "");
@Comment("Enable this when you allow registration through a website") @Comment("Enable this when you allow registration through a website")
@Type(SettingType.Boolean) public static final Property<Boolean> MYSQL_WEBSITE =
public boolean mySQLWebsite = false; newProperty(BOOLEAN, "DataSource.mySQLWebsite", false);
private static File configFile = new File("." + File.separator + "plugins" + File.separator + "AuthMe" + File.separator + "database.yml"); private DatabaseSettings() {
private DatabaseSettings instance;
public DatabaseSettings()
{
super(configFile);
instance = this;
} }
public DatabaseSettings getInstance() {
return instance;
}
public void setInstance(DatabaseSettings instance) {
this.instance = instance;
}
} }

View File

@ -1,83 +1,72 @@
package fr.xephi.authme.settings.custom; package fr.xephi.authme.settings.custom;
import java.io.File; import fr.xephi.authme.settings.custom.domain.Comment;
import java.util.ArrayList; import fr.xephi.authme.settings.custom.domain.Property;
import fr.xephi.authme.settings.custom.domain.SettingsClass;
import java.util.List; import java.util.List;
import fr.xephi.authme.settings.custom.annotations.Comment; import static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type; import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType; 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") @Comment("Email SMTP server host")
@Type(SettingType.String) public static final Property<String> SMTP_HOST =
public String mailSMTP = "smtp.gmail.com"; newProperty(STRING, "Email.mailSMTP", "smtp.gmail.com");
@Comment("Email SMTP server port") @Comment("Email SMTP server port")
@Type(SettingType.Int) public static final Property<Integer> SMTP_PORT =
public int mailPort = 465; newProperty(INTEGER, "Email.mailPort", 465);
@Comment("Email account whose send the mail") @Comment("Email account which sends the mails")
@Type(SettingType.String) public static final Property<String> MAIL_ACCOUNT =
public String mailAccount = ""; newProperty(STRING, "Email.mailAccount", "");
@Comment("Email account password") @Comment("Email account password")
@Type(SettingType.String) public static final Property<String> MAIL_PASSWORD =
public String mailPassword = ""; newProperty(STRING, "Email.mailPassword", "");
@Comment("Random password length") @Comment("Recovery password length")
@Type(SettingType.Int) public static final Property<Integer> RECOVERY_PASSWORD_LENGTH =
public int recoveryPasswordLength = 8; newProperty(INTEGER, "Email.RecoveryPasswordLength", 8);
@Comment("Mail Subject") @Comment("Mail Subject")
@Type(SettingType.String) public static final Property<String> RECOVERY_MAIL_SUBJECT =
public String mailSubject = "Your new AuthMe password"; newProperty(STRING, "Email.mailSubject", "Your new AuthMe password");
@Comment("Like maxRegPerIP but with email") @Comment("Like maxRegPerIP but with email")
@Type(SettingType.Int) public static final Property<Integer> MAX_REG_PER_EMAIL =
public int maxRegPerEmail = 1; newProperty(INTEGER, "Email.maxRegPerEmail", 1);
@Comment("Recall players to add an email ?") @Comment("Recall players to add an email?")
@Type(SettingType.Boolean) public static final Property<Boolean> RECALL_PLAYERS =
public boolean recallPlayers = false; newProperty(BOOLEAN, "Email.recallPlayers", false);
@Comment("Delay in minute for the recall scheduler") @Comment("Delay in minute for the recall scheduler")
@Type(SettingType.Int) public static final Property<Integer> DELAY_RECALL =
public int delayRecall = 5; newProperty(INTEGER, "Email.delayRecall", 5);
@Comment("Blacklist these domains for emails") @Comment("Blacklist these domains for emails")
@Type(SettingType.StringList) public static final Property<List<String>> DOMAIN_BLACKLIST =
public List<String> emailBlackListed = new ArrayList<String>(); newProperty(STRING_LIST, "Email.emailBlacklisted", "10minutemail.com");
@Comment("Whitelist ONLY these domains for emails") @Comment("Whitelist ONLY these domains for emails")
@Type(SettingType.StringList) public static final Property<List<String>> DOMAIN_WHITELIST =
public List<String> emailWhiteListed = new ArrayList<String>(); newProperty(STRING_LIST, "Email.emailWhitelisted");
@Comment("Do we need to send new password draw in an image ?") @Comment("Send the new password drawn in an image?")
@Type(SettingType.Boolean) public static final Property<Boolean> PASSWORD_AS_IMAGE =
public boolean generateImage = false; 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<String> 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;
}
} }

View File

@ -1,54 +1,39 @@
package fr.xephi.authme.settings.custom; package fr.xephi.authme.settings.custom;
import java.io.File; import fr.xephi.authme.settings.custom.domain.Comment;
import java.util.ArrayList; import fr.xephi.authme.settings.custom.domain.Property;
import java.util.List; 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 static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType;
public class HooksSettings extends CustomSetting { public class HooksSettings implements SettingsClass {
@Comment("Do we need to hook with multiverse for spawn checking?") @Comment("Do we need to hook with multiverse for spawn checking?")
@Type(SettingType.Boolean) public static final Property<Boolean> MULTIVERSE =
public boolean multiverse = true; newProperty(PropertyType.BOOLEAN, "Hooks.multiverse", true);
@Comment("Do we need to hook with BungeeCord ?") @Comment("Do we need to hook with BungeeCord?")
@Type(SettingType.Boolean) public static final Property<Boolean> BUNGEECORD =
public boolean bungeecord = false; newProperty(PropertyType.BOOLEAN, "Hooks.bungeecord", false);
@Comment("Send player to this BungeeCord server after register/login") @Comment("Send player to this BungeeCord server after register/login")
@Type(SettingType.String) public static final Property<String> BUNGEECORD_SERVER =
public String sendPlayerTo = ""; newProperty(PropertyType.STRING, "bungeecord.server", "");
@Comment("Do we need to disable Essentials SocialSpy on join?") @Comment("Do we need to disable Essentials SocialSpy on join?")
@Type(SettingType.Boolean) public static final Property<Boolean> DISABLE_SOCIAL_SPY =
public boolean disableSocialSpy = false; newProperty(PropertyType.BOOLEAN, "Hooks.disableSocialSpy", false);
@Comment("Do we need to force /motd Essentials command on join?") @Comment("Do we need to force /motd Essentials command on join?")
@Type(SettingType.Boolean) public static final Property<Boolean> USE_ESSENTIALS_MOTD =
public boolean useEssentialsMotd = false; newProperty(PropertyType.BOOLEAN, "Hooks.useEssentialsMotd", false);
@Comment("Do we need to cache custom Attributes?") @Comment("Do we need to cache custom Attributes?")
@Type(SettingType.Boolean) public static final Property<Boolean> CACHE_CUSTOM_ATTRIBUTES =
public boolean customAttributes = false; 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;
}
} }

View File

@ -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<Class<? extends SettingsClass>> 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> T getOption(Property<T> 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<String> currentPath = new ArrayList<>();
for (Map.Entry<Property, String[]> entry : properties.entrySet()) {
Property<?> property = entry.getKey();
// Handle properties
List<String> propertyPath = Arrays.asList(property.getPath().split("\\."));
List<String> commonPathParts = CollectionUtils.filterCommonStart(
currentPath, propertyPath.subList(0, propertyPath.size() - 1));
List<String> 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<String> 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;
}
}

View File

@ -1,63 +1,46 @@
package fr.xephi.authme.settings.custom; package fr.xephi.authme.settings.custom;
import java.io.File; import fr.xephi.authme.settings.custom.domain.Comment;
import java.util.ArrayList; import fr.xephi.authme.settings.custom.domain.Property;
import fr.xephi.authme.settings.custom.domain.SettingsClass;
import java.util.List; import java.util.List;
import fr.xephi.authme.settings.custom.annotations.Comment; import static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type; import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType; 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 )") public class ProtectionSettings implements SettingsClass {
@Type(SettingType.Boolean)
public boolean enableProtection = false; @Comment("Enable some servers protection (country based login, antibot)")
public static final Property<Boolean> 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", @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 !"}) "PLEASE USE QUOTES!"})
@Type(SettingType.StringList) public static final Property<List<String>> COUNTRIES_WHITELIST =
public List<String> countriesWhitelist = new ArrayList<String>(); newProperty(STRING_LIST, "Protection.countries", "US", "GB", "A1");
@Comment({"Countries not allowed to join the server and register", @Comment({"Countries not allowed to join the server and register",
"PLEASE USE QUOTES !"}) "PLEASE USE QUOTES!"})
@Type(SettingType.StringList) public static final Property<List<String>> COUNTRIES_BLACKLIST =
public List<String> countriesBlacklist = new ArrayList<String>(); newProperty(STRING_LIST, "Protection.countriesBlacklist");
@Comment("Do we need to enable automatic antibot system?") @Comment("Do we need to enable automatic antibot system?")
@Type(SettingType.Boolean) public static final Property<Boolean> ENABLE_ANTIBOT =
public boolean enableAntiBot = false; newProperty(BOOLEAN, "Protection.enableAntiBot", false);
@Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically")
@Type(SettingType.Int) public static final Property<Integer> ANTIBOT_SENSIBILITY =
public int antiBotSensibility = 5; newProperty(INTEGER, "Protection.antiBotSensibility", 5);
@Comment("Duration in minutes of the antibot automatic system") @Comment("Duration in minutes of the antibot automatic system")
@Type(SettingType.Int) public static final Property<Integer> ANTIBOT_DURATION =
public int antiBotDuration = 10; 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;
}
} }

View File

@ -1,62 +1,49 @@
package fr.xephi.authme.settings.custom; package fr.xephi.authme.settings.custom;
import java.io.File; import fr.xephi.authme.settings.custom.domain.Comment;
import java.util.ArrayList; import fr.xephi.authme.settings.custom.domain.Property;
import java.util.List; import fr.xephi.authme.settings.custom.domain.SettingsClass;
import fr.xephi.authme.settings.custom.annotations.Comment; import static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type; import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType; 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") @Comment("If enabled, AuthMe automatically purges old, unused accounts")
@Type(SettingType.Boolean) public static final Property<Boolean> USE_AUTO_PURGE =
public boolean useAutoPurge = false; newProperty(BOOLEAN, "Purge.useAutoPurge", false);
@Comment("Number of Days an account become Unused") @Comment("Number of Days an account become Unused")
@Type(SettingType.Int) public static final Property<Integer> DAYS_BEFORE_REMOVE_PLAYER =
public int daysBeforeRemovePlayer = 60; newProperty(INTEGER, "Purge.daysBeforeRemovePlayer", 60);
@Comment("Do we need to remove the player.dat file during purge process?") @Comment("Do we need to remove the player.dat file during purge process?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_PLAYER_DAT =
public boolean removePlayerDat = false; newProperty(BOOLEAN, "Purge.removePlayerDat", false);
@Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_ESSENTIALS_FILES =
public boolean removeEssentialsFiles = false; newProperty(BOOLEAN, "Purge.removeEssentialsFiles", false);
@Comment("World where are players.dat stores") @Comment("World where are players.dat stores")
@Type(SettingType.String) public static final Property<String> DEFAULT_WORLD =
public String defaultWorld = "world"; newProperty(STRING, "Purge.defaultWorld", "world");
@Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_LIMITED_CREATIVE_INVENTORIES =
public boolean removeLimiteCreativeInventories = false; newProperty(BOOLEAN, "Purge.removeLimitedCreativesInventories", false);
@Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_ANTI_XRAY_FILE =
public boolean removeAntiXRayFile = false; newProperty(BOOLEAN, "Purge.removeAntiXRayFile", false);
@Comment("Do we need to remove permissions?") @Comment("Do we need to remove permissions?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_PERMISSIONS =
public boolean removePermissions = false; 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;
}
} }

View File

@ -1,60 +1,51 @@
package fr.xephi.authme.settings.custom; package fr.xephi.authme.settings.custom;
import java.io.File; import fr.xephi.authme.settings.custom.domain.Comment;
import java.util.ArrayList; import fr.xephi.authme.settings.custom.domain.Property;
import java.util.List; import fr.xephi.authme.settings.custom.domain.SettingsClass;
import fr.xephi.authme.settings.custom.annotations.Comment; import static fr.xephi.authme.settings.custom.domain.Property.newProperty;
import fr.xephi.authme.settings.custom.annotations.Type; import static fr.xephi.authme.settings.custom.domain.PropertyType.BOOLEAN;
import fr.xephi.authme.settings.custom.annotations.Type.SettingType; 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", @Comment({"Stop the server if we can't contact the sql database",
"Take care with this, if you set that to false,", "Take care with this, if you set this to false,",
"AuthMe automatically disable and the server is not protected!"}) "AuthMe will automatically disable and the server won't be protected!"})
@Type(SettingType.Boolean) public static final Property<Boolean> STOP_SERVER_ON_PROBLEM =
public boolean stopServerOnProblem = true; newProperty(BOOLEAN, "Security.SQLProblem.stopServer", true);
@Comment("/reload support") @Comment("/reload support")
@Type(SettingType.Boolean) public static final Property<Boolean> USE_RELOAD_COMMAND_SUPPORT =
public boolean useReloadCommandSupport = true; newProperty(BOOLEAN, "Security.ReloadCommand.useReloadCommandSupport", true);
@Comment("Remove Spam from Console ?") @Comment("Remove spam from console?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_SPAM_FROM_CONSOLE =
public boolean removeSpamFromConsole = false; newProperty(BOOLEAN, "Security.console.noConsoleSpam", false);
@Comment("Remove Password from Console ?") @Comment("Remove passwords from console?")
@Type(SettingType.Boolean) public static final Property<Boolean> REMOVE_PASSWORD_FROM_CONSOLE =
public boolean removePasswordFromConsole = true; newProperty(BOOLEAN, "Security.console.removePassword", true);
@Comment("Player need to put a captcha when he fails too lot the password") @Comment("Player need to put a captcha when he fails too lot the password")
@Type(SettingType.Boolean) public static final Property<Boolean> USE_CAPTCHA =
public boolean useCaptcha = false; newProperty(BOOLEAN, "Security.captcha.useCaptcha", false);
@Comment("Max allowed tries before request a captcha") @Comment("Max allowed tries before request a captcha")
@Type(SettingType.Int) public static final Property<Integer> MAX_LOGIN_TRIES_BEFORE_CAPTCHA =
public int maxLoginTryBeforeCaptcha = 5; newProperty(INTEGER, "Security.captcha.maxLoginTry", 5);
@Comment("Captcha length ") @Comment("Captcha length")
@Type(SettingType.Int) public static final Property<Integer> CAPTCHA_LENGTH =
public int captchaLength = 5; 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<Boolean> 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;
}
} }

View File

@ -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;
}

View File

@ -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.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -6,15 +6,12 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/** /**
* * Comment for properties which are also included in the YAML file upon saving.
* Add a comment to a field value
*
* @author xephi59
*
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) @Target(ElementType.FIELD)
public @interface Comment { public @interface Comment {
public String[] value() default ""; String[] value() default "";
} }

View File

@ -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 <i>setting</i> that is read from the config.yml file).
*/
public class Property<T> {
private final PropertyType<T> type;
private final String path;
private final T defaultValue;
private Property(PropertyType<T> type, String path, T defaultValue) {
Objects.requireNonNull(defaultValue);
this.type = type;
this.path = path;
this.defaultValue = defaultValue;
}
public static <T> Property<T> newProperty(PropertyType<T> type, String path, T defaultValue) {
return new Property<>(type, path, defaultValue);
}
@SafeVarargs
public static <U> Property<List<U>> newProperty(PropertyType<List<U>> type, String path, U... defaultValues) {
return new Property<>(type, path, Arrays.asList(defaultValues));
}
// -----
// Overloaded convenience methods for specific types
// -----
public static Property<Boolean> newProperty(String path, boolean defaultValue) {
return new Property<>(PropertyType.BOOLEAN, path, defaultValue);
}
public static Property<Integer> newProperty(String path, int defaultValue) {
return new Property<>(PropertyType.INTEGER, path, defaultValue);
}
public static Property<String> 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<String> 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 + "'";
}
}

View File

@ -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 <T> The value of the property
* @see Property
*/
public abstract class PropertyType<T> {
public static final PropertyType<Boolean> BOOLEAN = new BooleanProperty();
public static final PropertyType<Double> DOUBLE = new DoubleProperty();
public static final PropertyType<Integer> INTEGER = new IntegerProperty();
public static final PropertyType<String> STRING = new StringProperty();
public static final PropertyType<List<String>> 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<T> 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<String> asYaml(Property<T> 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<String> asYaml(T value);
/**
* Boolean property.
*/
private static final class BooleanProperty extends PropertyType<Boolean> {
@Override
public Boolean getFromFile(Property<Boolean> property, YamlConfiguration configuration) {
return configuration.getBoolean(property.getPath(), property.getDefaultValue());
}
@Override
protected List<String> asYaml(Boolean value) {
return asList(value ? "true" : "false");
}
}
/**
* Double property.
*/
private static final class DoubleProperty extends PropertyType<Double> {
@Override
public Double getFromFile(Property<Double> property, YamlConfiguration configuration) {
return configuration.getDouble(property.getPath(), property.getDefaultValue());
}
@Override
protected List<String> asYaml(Double value) {
return asList(String.valueOf(value));
}
}
/**
* Integer property.
*/
private static final class IntegerProperty extends PropertyType<Integer> {
@Override
public Integer getFromFile(Property<Integer> property, YamlConfiguration configuration) {
return configuration.getInt(property.getPath(), property.getDefaultValue());
}
@Override
protected List<String> asYaml(Integer value) {
return asList(String.valueOf(value));
}
}
/**
* String property.
*/
private static final class StringProperty extends PropertyType<String> {
@Override
public String getFromFile(Property<String> property, YamlConfiguration configuration) {
return configuration.getString(property.getPath(), property.getDefaultValue());
}
@Override
protected List<String> 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<List<String>> {
@Override
public List<String> getFromFile(Property<List<String>> property, YamlConfiguration configuration) {
if (!configuration.isList(property.getPath())) {
return property.getDefaultValue();
}
return configuration.getStringList(property.getPath());
}
@Override
protected List<String> asYaml(List<String> value) {
if (value.isEmpty()) {
return asList("[]");
}
List<String> resultLines = new ArrayList<>();
resultLines.add(""); // add
for (String entry : value) {
// TODO: StringProperty#toYamlLiteral will return List<String>...
resultLines.add(" - " + StringProperty.toYamlLiteral(entry));
}
return resultLines;
}
}
}

View File

@ -0,0 +1,7 @@
package fr.xephi.authme.settings.custom.domain;
/**
* Marker for classes that define {@link Property} fields.
*/
public interface SettingsClass {
}

View File

@ -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.
* <p>
* 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:<ul>
* <li>"animal" (or any of its children) is sorted before "color" (or any of its children)</li>
* <li>"animal.bird" or any child thereof is sorted before "animal.rodent"</li>
* <li>"animal.rodent.rat" comes before "animal.rodent.rabbit"</li>
* </ul>
*
* @see PropertyMapComparator
*/
final class Node {
private final String name;
private final List<Node> 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 + "'";
}
}

View File

@ -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<Property, String[]>} for storing properties and their associated
* comments with custom ordering.
*
* @see PropertyMapComparator for details about the map's order
*/
public class PropertyMap {
private Map<Property, String[]> 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<Map.Entry<Property, String[]>> entrySet() {
return propertyMap.entrySet();
}
}

View File

@ -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:
* <ul>
* <li>are grouped by path, e.g. all "DataSource.mysql" properties are together, and "DataSource.mysql" properties
* are within the broader "DataSource" group.</li>
* <li>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.</li>
* </ul>
*/
final class PropertyMapComparator implements Comparator<Property> {
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;
}
}

View File

@ -3,6 +3,7 @@ package fr.xephi.authme.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* Utils class for collections. * Utils class for collections.
@ -58,4 +59,17 @@ public final class CollectionUtils {
public static <T> boolean isEmpty(Collection<T> coll) { public static <T> boolean isEmpty(Collection<T> coll) {
return coll == null || coll.isEmpty(); return coll == null || coll.isEmpty();
} }
public static <T> List<T> filterCommonStart(List<T> coll1, List<T> coll2) {
List<T> 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;
}
} }

View File

@ -117,4 +117,12 @@ public final class StringUtils {
return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); 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();
}
} }

View File

@ -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<Integer> 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<String>() {
@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<String> 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<Integer> DURATION_IN_SECONDS =
newProperty("test.duration", 4);
public static final Property<String> SYSTEM_NAME =
newProperty("test.systemName", "[TestDefaultValue]");
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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