9 using Newtonsoft.Json.Linq;
15 using System.Text.RegularExpressions;
65 protected void OnConfigChanged(
string configName,
object? prevValue,
object? newValue, Type cfgType) {
102 m_configDictionary =
new JObject();
104 m_defaultConfig = LoadDefaultConfig();
108 [GeneratedRegex(
@"^([A-z_][A-z0-9_]+)(\.[A-z_][A-z0-9_]+)+?[^\.]$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
109 public static partial Regex ConfigDotNotation();
111 protected JObject m_configDictionary;
112 protected JObject m_defaultConfig;
118 protected JObject LoadDefaultConfig() {
119 using var rStream = typeof(
ConfigManager).Assembly.GetManifestResourceStream(
"Hermod.Config.Resources.DefaultConfig.json");
120 if (rStream is
null) {
121 throw new Exception(
"Failed to load default configuration! Hermod will abort!");
124 using (var sReader =
new StreamReader(rStream)) {
125 var text = sReader.ReadToEnd();
126 return JObject.Parse(text);
130 private FileInfo? m_configFile =
null;
137 public FileInfo ConfigFile {
140 if (value == m_configFile) {
return; }
142 m_configFile = value;
154 public async Task LoadConfigAsync() {
155 if (!ConfigFile.Exists || ConfigFile.Length < 10) {
160 m_configDictionary = m_defaultConfig;
163 await SaveConfigAsync();
167 using (var cfgFile = ConfigFile.Open(FileMode.Open))
168 using (var sReader =
new StreamReader(cfgFile)) {
169 var sBuilder =
new StringBuilder();
171 while (sReader.Peek() != -1) {
172 sBuilder.AppendLine(await sReader.ReadLineAsync());
178 m_configDictionary = JObject.Parse(sBuilder.ToString());
181 }
catch (Exception ex) {
182 Console.Error.WriteLine(
"An error occurred while loading configs! Loading defaults!");
183 Console.Error.WriteLine($
"Error message: { ex.Message }");
186 Console.Error.WriteLine(ex.StackTrace);
195 public void LoadConfig() {
196 if (!ConfigFile.Exists || ConfigFile.Length < 10) {
201 m_configDictionary = m_defaultConfig;
208 using (var cfgFile = ConfigFile.Open(FileMode.Open))
209 using (var sReader =
new StreamReader(cfgFile)) {
210 var sBuilder =
new StringBuilder();
212 while (sReader.Peek() != -1) {
213 sBuilder.AppendLine(sReader.ReadLine());
219 m_configDictionary = JObject.Parse(sBuilder.ToString());
222 }
catch (Exception ex) {
223 Console.Error.WriteLine(
"An error occurred while loading configs! Loading defaults!");
224 Console.Error.WriteLine($
"Error message: { ex.Message }");
227 Console.Error.WriteLine(ex.StackTrace);
240 public async Task SaveConfigAsync() {
241 if (!ConfigFile.Exists) {
243 ConfigFile.Directory?.Create();
244 }
catch (UnauthorizedAccessException ex) {
245 HandleUnauthorizedAccessWhenSavingConfig(ex);
246 await SaveConfigAsync();
248 ConfigFile.Create().Close();
251 using (var cfgFile = ConfigFile.Open(FileMode.Truncate))
252 using (var sWriter =
new StreamWriter(cfgFile)) {
253 string serialisedData;
256 serialisedData = JsonConvert.SerializeObject(m_configDictionary, Formatting.Indented);
259 await sWriter.WriteLineAsync(serialisedData);
263 private void HandleUnauthorizedAccessWhenSavingConfig(UnauthorizedAccessException ex) {
264 AppLogger?.Error($
"Failed to create config file in { ConfigFile.FullName }!");
265 AppLogger?.Error(ex,
"Hermod does not have sufficient access rights.");
266 AppLogger?.Warning(
"Switching to user-local config location!");
267 ConfigFile = AppInfo.GetLocalHermodDirectory().CreateSubdirectory(AppInfo.HermodAppCfgDirName).GetSubFile(
DefaultConfigFileName);
276 public void SaveConfig() {
277 if (!ConfigFile.Exists) {
279 ConfigFile.Directory?.Create();
280 }
catch (UnauthorizedAccessException ex) {
281 HandleUnauthorizedAccessWhenSavingConfig(ex);
284 ConfigFile.Create().Close();
287 using (var cfgFile = ConfigFile.Open(FileMode.Truncate))
288 using (var sWriter =
new StreamWriter(cfgFile)) {
289 string serialisedData;
292 serialisedData = JsonConvert.SerializeObject(m_configDictionary, Formatting.Indented);
295 sWriter.WriteLine(serialisedData);
316 public T GetConfig<T>(
string configName) {
318 return GetConfig<T>(configName, m_configDictionary);
319 }
catch (ConfigNotFoundException) {
320 return GetConfig<T>(configName, m_defaultConfig);
332 public void SetConfig<T>(
string configName, T? configValue) => SetConfig<T>(configName, configValue, ref m_configDictionary);
343 protected void SetConfig<T>(
string configName, T? configValue, ref JObject dict) {
344 if (
string.IsNullOrEmpty(configName) ||
string.IsNullOrWhiteSpace(configName)) {
345 throw new ArgumentNullException(nameof(configName),
"The config name must not be null or empty!");
348 if (ConfigDotNotation().IsMatch(configName)) {
349 var periodPos = configName.IndexOf(
'.');
350 var container = configName.Substring(0, periodPos);
351 var subConfig = configName.Substring(periodPos + 1);
352 var subConfigHasDotNotation = ConfigDotNotation().IsMatch(subConfig);
354 if (dict.ContainsKey(container) && subConfigHasDotNotation) {
355 var token = dict[container];
356 if (token?.Type != JTokenType.Object) {
357 throw new ConfigException($
"Config type mismatch! Expected object; got { token?.Type.ToString() }");
360 var tokenObj = (JObject)token;
361 SetConfig<T>(subConfig, configValue, ref tokenObj);
363 }
else if (subConfigHasDotNotation) {
367 dict.Add(container,
new JObject());
370 SetConfig<T>(configName, configValue, ref dict);
375 SetConfig<T>(subConfig, configValue, ref dict);
378 if (!dict.ContainsKey(configName)) {
381 dict.Add(configName, JToken.FromObject(configValue));
384 ConfigChanged?.Invoke(
this,
new ConfigChangedEventArgs(configName,
null, configValue, typeof(T)));
388 var prevValue = dict[configName];
391 dict[configName] = JToken.FromObject(configValue);
394 ConfigChanged?.Invoke(
this,
new ConfigChangedEventArgs(configName, prevValue, configValue, typeof(T)));
407 protected T GetConfig<T>(
string configName, JObject dict) {
408 if (
string.IsNullOrEmpty(configName) ||
string.IsNullOrWhiteSpace(configName)) {
409 throw new ArgumentNullException(nameof(configName),
"The config name must not be null or empty!");
412 if (ConfigDotNotation().IsMatch(configName)) {
413 var periodPos = configName.IndexOf(
'.');
414 var container = configName.Substring(0, periodPos);
416 if (dict.ContainsKey(container)) {
417 var token = dict[container];
418 if (token?.Type != JTokenType.Object) {
419 throw new ConfigException($
"Config type mismatch! Expected object; got { token?.Type.ToString() }");
422 return GetConfig<T>(configName.Substring(periodPos + 1), (JObject)token);
424 throw new ConfigNotFoundException(container, $
"Could not find container for { configName }!");
428 if (!dict.ContainsKey(configName)) {
429 throw new ConfigNotFoundException(configName,
"Could not find requested config key!");
435 config = dict[configName];
439 return config.ToObject<T>();
442 #region Special Accessors
447 public LoggerConfig GetConsoleLoggerConfig() => GetConfig<LoggerConfig>(
"Logging.ConsoleLogging");
453 public LoggerConfig GetFileLoggerConfig() => GetConfig<LoggerConfig>(
"Logging.FileLogging");
459 public DirectoryInfo GetPluginInstallDir() {
460 var installDir = GetConfig<string?>(
"Plugins.InstallDir");
462 if (
string.IsNullOrEmpty(installDir)) {
463 installDir = AppInfo.GetLocalHermodDirectory().CreateSubdirectory(AppInfo.HermodAppPluginDirName).FullName;
464 SetConfig<string>(
"Plugins.InstallDir", installDir);
467 return new DirectoryInfo(installDir);
Event arguments for when a configuration has changed.
Provides an application-wide, thread safe way of getting and setting configurations relevant to the m...
volatile string m_lockedBy
ConfigChangedEventHandler? ConfigChanged
static ? FileInfo _defaultConfigPathCache
static ConfigManager Instance
Gets the application-wide instance of the ConfigManager.
void OnConfigChanged(string configName, object? prevValue, object? newValue, Type cfgType)
FileInfo GetDefaultConfigPath()
ILogger? AppLogger
Gets or sets the logger instance.
const string DefaultConfigFileName
static ? ConfigManager _instance
ConfigLoadedEventHandler? ConfigLoaded
Static class containing basic information for and about the application.
static string HermodAppCfgDirName
Gets the name of the application's config directory.
static DirectoryInfo GetBaseHermodDirectory()
Gets the application's base data directory.
Generic contract defining the behaviour when a config was changed.
Interface defining a contract which notifies anyone whom it may concern when the application configur...
delegate void ConfigChangedEventHandler(object? sender, ConfigChangedEventArgs e)
delegate void ConfigLoadedEventHandler(object? sender, ConfigLoadedEventArgs e)