Configuration.java
package dev.aura.bungeechat.config;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueFactory;
import dev.aura.bungeechat.BungeeChat;
import dev.aura.bungeechat.api.BungeeChatApi;
import dev.aura.bungeechat.util.LoggerHelper;
import dev.aura.bungeechat.util.MapUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Cleanup;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Delegate;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
@SuppressFBWarnings(
value = {"RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", "JLM_JSR166_UTILCONCURRENT_MONITORENTER"},
justification = "Code is generated by lombok which means I don't have any influence on it.")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Configuration implements Config {
protected static final ConfigParseOptions PARSE_OPTIONS =
ConfigParseOptions.defaults().setAllowMissing(false).setSyntax(ConfigSyntax.CONF);
protected static final ConfigRenderOptions RENDER_OPTIONS =
ConfigRenderOptions.defaults().setOriginComments(false).setJson(false);
protected static final String CONFIG_FILE_NAME = "config.conf";
protected static final String OLD_CONFIG_FILE_NAME = "config.yml";
protected static final String OLD_OLD_CONFIG_FILE_NAME = "config.old.yml";
protected static final File CONFIG_FILE =
new File(BungeeChat.getInstance().getConfigFolder(), CONFIG_FILE_NAME);
protected static final File OLD_CONFIG_FILE =
new File(CONFIG_FILE.getParentFile(), OLD_CONFIG_FILE_NAME);
protected static final File OLD_OLD_CONFIG_FILE =
new File(CONFIG_FILE.getParentFile(), OLD_OLD_CONFIG_FILE_NAME);
@Getter(value = AccessLevel.PROTECTED, lazy = true)
private static final String header = loadHeader();
private static Configuration currentConfig;
@Delegate protected Config config;
/**
* Creates and loads the config. Also saves it so that all missing values exist!<br>
* Also set currentConfig to this config.
*
* @return a configuration object, loaded from the config file.
*/
public static Configuration load() {
Configuration config = new Configuration();
config.loadConfig();
currentConfig = config;
return currentConfig;
}
public static Configuration get() {
return currentConfig;
}
private static String loadHeader() {
StringBuilder header = new StringBuilder();
try {
@Cleanup
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
BungeeChat.getInstance().getResourceAsStream(CONFIG_FILE_NAME),
StandardCharsets.UTF_8));
String line;
do {
line = reader.readLine();
if (line == null) throw new IOException("Unexpected EOF while reading " + CONFIG_FILE_NAME);
header.append(line).append('\n');
} while (line.startsWith("#"));
} catch (IOException e) {
LoggerHelper.error("Error loading file header", e);
}
return header.toString();
}
private static Collection<String> getPaths(ConfigValue config) {
if (config instanceof ConfigObject) {
return new ArrayList<>(((ConfigObject) config).keySet());
} else {
return Collections.emptyList();
}
}
private static List<String> getComment(Config config, String path) {
return config.hasPath(path) ? getComment(config.getValue(path)) : Collections.emptyList();
}
private static List<String> getComment(ConfigValue config) {
return config.origin().comments();
}
protected void loadConfig() {
boolean saveConfig = true;
final Config defaultConfig =
ConfigFactory.parseReader(
new InputStreamReader(
BungeeChat.getInstance().getResourceAsStream(CONFIG_FILE_NAME),
StandardCharsets.UTF_8),
PARSE_OPTIONS);
final Config strippedDefaultConfig = defaultConfig.withoutPath("ServerAlias");
if (CONFIG_FILE.exists()) {
try {
Config fileConfig = ConfigFactory.parseFile(CONFIG_FILE, PARSE_OPTIONS);
config = fileConfig.withFallback(strippedDefaultConfig);
} catch (ConfigException e) {
LoggerHelper.error(
"====================================================================================================");
LoggerHelper.error("Error while reading config:\n" + e.getLocalizedMessage());
LoggerHelper.error(
"The plugin will run with the default config (but the config file has not been changed)!");
LoggerHelper.error(
"After you fixed the issue, either restart the server or run `/bungeechat reload`.");
LoggerHelper.error(
"====================================================================================================");
saveConfig = false;
config = defaultConfig;
}
} else {
config = defaultConfig;
}
config = config.resolve();
convertOldConfig();
// Reapply default config. By default this does nothing but it can fix the missing config
// settings in some cases
config = config.withFallback(strippedDefaultConfig);
copyComments(defaultConfig);
if (saveConfig) saveConfig();
}
protected void saveConfig() {
try {
@Cleanup PrintWriter writer = new PrintWriter(CONFIG_FILE, StandardCharsets.UTF_8.name());
String renderedConfig = config.root().render(RENDER_OPTIONS);
renderedConfig = getHeader() + renderedConfig;
writer.print(renderedConfig);
} catch (FileNotFoundException | UnsupportedEncodingException e) {
LoggerHelper.error("Something very unexpected happened! Please report this!", e);
}
}
@SuppressFBWarnings(
value = {
"SF_SWITCH_FALLTHROUGH",
"SF_SWITCH_NO_DEFAULT",
"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"
},
justification =
"Fallthrough intended\n"
+ "Wrong detection by SpotBugs\n"
+ "If we can't delete the file, we just ignore it. Really nothing we care about.")
private void convertOldConfig() {
if (OLD_CONFIG_FILE.exists()) {
convertYAMLConfig();
}
switch (String.format(Locale.ROOT, "%.1f", config.getDouble("Version"))) {
case "11.0":
LoggerHelper.info("Performing config migration 11.0 -> 11.1 ...");
// Rename "passToClientServer" to "passToBackendServer"
for (String basePath :
new String[] {"Modules.GlobalChat", "Modules.LocalChat", "Modules.StaffChat"}) {
final String newPath = basePath + ".passToBackendServer";
final String oldPath = basePath + ".passToClientServer";
// Remove old path first to reduce the amount of data that needs to be copied
config = config.withoutPath(oldPath).withValue(newPath, config.getValue(oldPath));
}
case "11.1":
LoggerHelper.info("Performing config migration 11.1 -> 11.2 ...");
// Delete old language files
final File langDir = BungeeChat.getInstance().getLangFolder();
File langFile;
for (String lang :
new String[] {"de_DE", "en_US", "fr_FR", "hu_HU", "nl_NL", "pl_PL", "ru_RU", "zh_CN"}) {
langFile = new File(langDir, lang + ".lang");
if (langFile.exists()) {
langFile.delete();
}
}
case "11.2":
LoggerHelper.info("Performing config migration 11.2 -> 11.3 ...");
// Remove config section "Modules.TabCompletion"
config = config.withoutPath("Modules.TabCompletion");
case "11.3":
LoggerHelper.info("Performing config migration 11.3 -> 11.4 ...");
final Config globalServerList = config.getConfig("Modules.GlobalChat.serverList");
// Copy over server list from Global to AutoBroadcast if it is enabled
if (globalServerList.getBoolean("enabled")) {
config = config.withValue("Modules.AutoBroadcast.serverList", globalServerList.root());
}
case "11.4":
LoggerHelper.info("Performing config migration 11.4 -> 11.5 ...");
// Move the server lists section one layer down
config =
config.withValue(
"Modules.MulticastChat.serverLists",
config.getValue("Modules.MulticastChat.serverLists.lists"));
case "11.5":
LoggerHelper.info("Performing config migration 11.5 -> 11.6 ...");
// Rename PrefixDefaults to PrefixSuffixSettings
config =
config
.withoutPath("PrefixDefaults")
.withValue("PrefixSuffixSettings", config.getValue("PrefixDefaults"));
case "11.6":
LoggerHelper.info("Performing config migration 11.6 -> 11.7 ...");
// Rename blockedcommands to blockedCommands
config =
config
.withoutPath("Modules.Muting.blockedcommands")
.withValue(
"Modules.Muting.blockedCommands",
config.getValue("Modules.Muting.blockedcommands"));
case "11.7":
LoggerHelper.info("Performing config migration 11.7 -> 11.8 ...");
// Only added PrefixSuffixSettings.forceEnable
default:
// Unknown Version or old version
// -> Update version
config =
config.withValue(
"Version", ConfigValueFactory.fromAnyRef(BungeeChatApi.CONFIG_VERSION));
case "11.8":
// Up to date
// -> No action needed
}
}
@SuppressFBWarnings(
value = "REC_CATCH_EXCEPTION",
justification = "Behavior intended. I want to catch ALL exceptions.")
private void convertYAMLConfig() {
try {
LoggerHelper.warning("Detected old YAML config. Trying to migrate to HOCON.");
// Read old config
@Cleanup
InputStreamReader reader =
new InputStreamReader(new FileInputStream(OLD_CONFIG_FILE), StandardCharsets.UTF_8);
net.md_5.bungee.config.Configuration oldConfig =
ConfigurationProvider.getProvider(YamlConfiguration.class).load(reader);
net.md_5.bungee.config.Configuration section;
// Close file
reader.close();
// Migrate settings
section = oldConfig.getSection("AccountDataBase");
final ImmutableMap<String, Object> accountDatabase =
ImmutableMap.<String, Object>builder()
.put("database", section.getString("database"))
.put("enabled", section.getBoolean("enabled"))
.put("ip", section.getString("ip"))
.put("password", section.getString("password"))
.put("port", section.getInt("port"))
.put("tablePrefix", section.getString("tablePrefix"))
.put("user", section.getString("user"))
.build();
section = oldConfig.getSection("Formats");
final ImmutableMap<String, Object> formats =
ImmutableMap.<String, Object>builder()
.put("alert", section.getString("alert"))
.put("chatLoggingConsole", section.getString("chatLoggingConsole"))
.put("chatLoggingFile", section.getString("chatLoggingFile"))
.put("globalChat", section.getString("globalChat"))
.put("helpOp", section.getString("helpOp"))
.put("joinMessage", section.getString("joinMessage"))
.put("leaveMessage", section.getString("leaveMessage"))
.put("localChat", section.getString("localChat"))
.put("localSpy", section.getString("localSpy"))
.put("messageSender", section.getString("messageSender"))
.put("messageTarget", section.getString("messageTarget"))
.put(
"motd",
String.join("\n", oldConfig.getStringList("Settings.Modules.MOTD.message")))
.put("serverSwitch", section.getString("serverSwitch"))
.put("socialSpy", section.getString("socialSpy"))
.put("staffChat", section.getString("staffChat"))
.put(
"welcomeMessage",
String.join(
"\n", oldConfig.getStringList("Settings.Modules.WelcomeMessage.message")))
.build();
final net.md_5.bungee.config.Configuration modulesSection =
oldConfig.getSection("Settings.Modules");
section = modulesSection.getSection("Alert");
final ImmutableMap<String, Object> moduleAlert =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("AntiAdvertising");
final ImmutableMap<String, Object> moduleAntiAdvertising =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.put("whitelisted", section.getStringList("whitelisted"))
.build();
section = modulesSection.getSection("AntiDuplication");
final ImmutableMap<String, Object> moduleAntiDuplication =
ImmutableMap.<String, Object>builder()
.put("checkPastMessages", section.getInt("checkPastMessages"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("AntiSwear");
final ImmutableMap<String, Object> moduleAntiSwear =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.put("freeMatching", section.getBoolean("freeMatching"))
.put("ignoreDuplicateLetters", section.getBoolean("ignoreDuplicateLetters"))
.put("ignoreSpaces", section.getBoolean("ignoreSpaces"))
.put("leetSpeak", section.getBoolean("leetSpeak"))
.put("replacement", section.getString("replacement"))
.put("words", section.getStringList("words"))
.build();
section = modulesSection.getSection("AutoBroadcast");
final ImmutableMap<String, Object> moduleAutoBroadcast =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.put("interval", section.getInt("interval") + "s")
.put("messages", section.getStringList("messages"))
.put("random", section.getBoolean("random"))
.build();
section = modulesSection.getSection("ChatLock");
final ImmutableMap<String, Object> moduleChatLock =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("emptyLinesOnClear", section.getInt("emptyLinesOnClear"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("ChatLogging");
final ImmutableMap<String, Object> moduleChatLogging =
ImmutableMap.<String, Object>builder()
.put("console", section.getBoolean("console"))
.put("enabled", section.getBoolean("enabled"))
.put("file", section.getBoolean("file"))
.put("filteredCommands", section.getStringList("filteredCommands"))
.put("logFile", section.getString("logFile"))
.put("privateMessages", section.getBoolean("privateMessages"))
.build();
section = modulesSection.getSection("ClearChat");
final ImmutableMap<String, Object> moduleClearChat =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("emptyLinesOnClear", section.getInt("emptyLinesOnClear"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("GlobalChat");
final ImmutableMap<String, Object> moduleGlobalChatServerList =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("serverList.enabled"))
.put("list", section.getStringList("serverList.list"))
.build();
final ImmutableMap<String, Object> moduleGlobalChatSymbol =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("symbol.enabled"))
.put("symbol", section.getString("symbol.symbol"))
.build();
final ImmutableMap<String, Object> moduleGlobalChat =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("default", section.getBoolean("default"))
.put("enabled", section.getBoolean("enabled"))
.put("passToBackendServer", section.getBoolean("passToClientServer"))
.put("serverList", moduleGlobalChatServerList)
.put("symbol", moduleGlobalChatSymbol)
.build();
section = modulesSection.getSection("HelpOp");
final ImmutableMap<String, Object> moduleHelpOp =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("Ignoring");
final ImmutableMap<String, Object> moduleIgnoring =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("JoinMessage");
final ImmutableMap<String, Object> moduleJoinMessage =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("LeaveMessage");
final ImmutableMap<String, Object> moduleLeaveMessage =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("LocalChat");
final ImmutableMap<String, Object> moduleLocalChat =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.put("passToBackendServer", section.getBoolean("passToClientServer"))
.build();
section = modulesSection.getSection("MOTD");
final ImmutableMap<String, Object> moduleMOTD =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("Messenger");
final ImmutableMap<String, Object> moduleMessengerAliases =
ImmutableMap.<String, Object>builder()
.put("message", section.getStringList("aliases.message"))
.put("msgtoggle", section.getStringList("aliases.msgtoggle"))
.put("reply", section.getStringList("aliases.reply"))
.build();
final ImmutableMap<String, Object> moduleMessenger =
ImmutableMap.<String, Object>builder()
.put("aliases", moduleMessengerAliases)
.put("enabled", section.getBoolean("enabled"))
.put("filterPrivateMessages", section.getBoolean("filterMessages"))
.build();
section = modulesSection.getSection("Muting");
final ImmutableMap<String, Object> moduleMutingAliases =
ImmutableMap.<String, Object>builder()
.put("mute", section.getStringList("aliases.mute"))
.put("tempmute", section.getStringList("aliases.tempmute"))
.put("unmute", section.getStringList("aliases.unmute"))
.build();
final ImmutableMap<String, Object> moduleMuting =
ImmutableMap.<String, Object>builder()
.put("aliases", moduleMutingAliases)
.put("blockedcommands", section.getStringList("blockedcommands"))
.put("disableWithOtherMutePlugins", section.getBoolean("disableWithOtherMutePlugins"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("ServerSwitchMessages");
final ImmutableMap<String, Object> moduleServerSwitchMessages =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("Spy");
final ImmutableMap<String, Object> moduleSpyAliases =
ImmutableMap.<String, Object>builder()
.put("localspy", section.getStringList("aliases.localspy"))
.put("socialspy", section.getStringList("aliases.socialspy"))
.build();
final ImmutableMap<String, Object> moduleSpy =
ImmutableMap.<String, Object>builder()
.put("aliases", moduleSpyAliases)
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("StaffChat");
final ImmutableMap<String, Object> moduleStaffChat =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("enabled", section.getBoolean("enabled"))
.put("passToBackendServer", section.getBoolean("passToClientServer"))
.build();
section = modulesSection.getSection("Vanish");
final ImmutableMap<String, Object> moduleVanish =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("VersionChecker");
final ImmutableMap<String, Object> moduleVersionChecker =
ImmutableMap.<String, Object>builder()
.put("checkOnAdminLogin", section.getBoolean("checkOnAdminLogin"))
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("WelcomeMessage");
final ImmutableMap<String, Object> moduleWelcomeMessage =
ImmutableMap.<String, Object>builder()
.put("enabled", section.getBoolean("enabled"))
.build();
section = modulesSection.getSection("LocalTo");
final ImmutableMap<String, Object> moduleLocalTo =
ImmutableMap.<String, Object>builder()
.put("aliases", section.getStringList("aliases"))
.put("enabled", section.getBoolean("enabled"))
.build();
final ImmutableMap<String, Object> modules =
ImmutableMap.<String, Object>builder()
.put("Alert", moduleAlert)
.put("AntiAdvertising", moduleAntiAdvertising)
.put("AntiDuplication", moduleAntiDuplication)
.put("AntiSwear", moduleAntiSwear)
.put("AutoBroadcast", moduleAutoBroadcast)
.put("ChatLock", moduleChatLock)
.put("ChatLogging", moduleChatLogging)
.put("ClearChat", moduleClearChat)
.put("GlobalChat", moduleGlobalChat)
.put("HelpOp", moduleHelpOp)
.put("Ignoring", moduleIgnoring)
.put("JoinMessage", moduleJoinMessage)
.put("LeaveMessage", moduleLeaveMessage)
.put("LocalChat", moduleLocalChat)
.put("MOTD", moduleMOTD)
.put("Messenger", moduleMessenger)
.put("Muting", moduleMuting)
.put("ServerSwitchMessages", moduleServerSwitchMessages)
.put("Spy", moduleSpy)
.put("StaffChat", moduleStaffChat)
.put("Vanish", moduleVanish)
.put("VersionChecker", moduleVersionChecker)
.put("WelcomeMessage", moduleWelcomeMessage)
.put("LocalTo", moduleLocalTo)
.build();
section = oldConfig.getSection("Settings.PermissionsManager");
final ImmutableMap<String, Object> permissionsManager =
ImmutableMap.<String, Object>builder()
.put("defaultPrefix", section.getString("defaultPrefix"))
.put("defaultSuffix", section.getString("defaultSuffix"))
.build();
section = oldConfig.getSection("Settings.ServerAlias");
final ImmutableMap<String, String> serverAlias =
section.getKeys().stream()
.collect(MapUtils.immutableMapCollector(key -> key, section::getString));
final ImmutableMap<String, Object> configMap =
ImmutableMap.<String, Object>builder()
.put("AccountDatabase", accountDatabase)
.put("Formats", formats)
.put("Modules", modules)
.put("PrefixDefaults", permissionsManager)
.put("ServerAlias", serverAlias)
.build();
config =
ConfigFactory.parseMap(configMap)
.withFallback(config.withoutPath("ServerAlias"))
.resolve()
.withValue("Version", ConfigValueFactory.fromAnyRef("11.3"));
// Rename old file
Files.move(OLD_CONFIG_FILE, OLD_OLD_CONFIG_FILE);
LoggerHelper.info("Old config file has been renamed to config.old.yml.");
// Done
LoggerHelper.info("Migration successful!");
} catch (Exception e) {
LoggerHelper.error("There has been an error while migrating the old YAML config file!", e);
}
}
private void copyComments(Config defaultConfig) {
final Queue<String> paths = new LinkedList<>(getPaths(config.root()));
while (!paths.isEmpty()) {
final String path = paths.poll();
final ConfigValue currentConfig = config.getValue(path);
// Add new paths to path list
paths.addAll(
getPaths(currentConfig).stream()
.map(newPath -> path + '.' + newPath)
.collect(Collectors.toList()));
// If the current value has a comment we will not override it
if (!getComment(currentConfig).isEmpty()) continue;
final List<String> comments = getComment(defaultConfig, path);
// If the default config has no comments we can't set any
if (comments.isEmpty()) continue;
// Set comment
config =
config.withValue(
path, currentConfig.withOrigin(currentConfig.origin().withComments(comments)));
}
}
}