You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

268 lines
10 KiB

using IPA.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using IniParser;
using IniParser.Model;
using System.Reflection;
namespace IPA.Config.ConfigProviders
{
[Config.Type("ini")]
internal class IniConfigProvider : IConfigProvider
{
public static void RegisterConfig()
{
Config.Register<IniConfigProvider>();
}
private IniData _iniData;
// TODO: create a wrapper that allows empty object creation
public dynamic Dynamic => _iniData;
public bool HasChanged { get; private set; }
public bool InMemoryChanged { get; set; }
public DateTime LastModified => File.GetLastWriteTime(Filename);
private string _filename;
public string Filename
{
get => _filename;
set
{
if (_filename != null)
throw new InvalidOperationException("Can only assign to Filename once");
_filename = value;
}
}
// Load file
public void Load()
{
Logger.config.Debug($"Loading file {Filename}");
var fileInfo = new FileInfo(Filename);
if (fileInfo.Exists)
{
try
{
var parser = new FileIniDataParser();
parser.Parser.Configuration.CaseInsensitive = true;
_iniData = parser.ReadFile(fileInfo.FullName);
}
catch (Exception e)
{
Logger.config.Error($"Error parsing INI in file {Filename}; resetting to empty INI");
Logger.config.Error(e);
_iniData = new IniData();
}
}
else
{
Logger.config.Debug($"File {fileInfo.FullName} doesn't exist");
_iniData = new IniData();
}
InMemoryChanged = true;
}
// This is basically trying to deserialize from INI data to a config object
public T Parse<T>()
{
// Create an instance of the config object to return
T configObj = Activator.CreateInstance<T>();
// Get a list of the fields declared in the config object
Type configObjType = typeof(T);
// Create a dictionary to record which fields are found in the class files
Dictionary<string, FieldInfo> classConfigField = new Dictionary<string, FieldInfo>();
// This goes through each field of the class to set values
// if found in the configuration file
foreach (FieldInfo field in configObjType.GetFields())
{
Type fieldType = field.FieldType;
// If thie field is an object, loop through its fields ("subfields")
if (Type.GetTypeCode(fieldType) == TypeCode.Object)
{
// Get the sub object value from the config object
object configObjSubObj = field.GetValue(configObj);
foreach (FieldInfo subField in fieldType.GetFields())
{
// If the INI file has a section/key pair corresponding to the field/subfield,
// set the subfield value and store the field info in dictionary
if (_iniData.Sections.ContainsSection(field.Name) && _iniData[field.Name].ContainsKey(subField.Name))
{
SetFieldValue(subField, configObjSubObj, _iniData[field.Name][subField.Name]);
string fieldName = field.Name + "." + subField.Name;
classConfigField[fieldName.ToUpper()] = subField;
}
else
{
Logger.config.Debug($"{field.Name}.{subField.Name} doesn't have a configuration value! Keeping existing value {subField.GetValue(configObjSubObj)}");
}
}
}
else
{
// If a field in the configuration object isn't itself an object, then it's a primitive type
// declared in the global section of the INI file
if (_iniData.Global.ContainsKey(field.Name))
{
SetFieldValue(field, configObj, _iniData.Global[field.Name]);
}
else
{
Logger.config.Debug($"{field.Name} doesn't have a configuration value! Keeping existing value {field.GetValue(configObj)}");
}
string fieldName = field.Name;
// store field info in dictionary (case insensitive)
classConfigField[fieldName.ToUpper()] = field;
}
}
// Loop through the global section of the INI file and see if any of those keys
// don't correspond to a field in the object class by using dictionary
// If any of them don't correspond to a field in the object class, add a comment to INI file
// mentioning those keys are being ignored
foreach (KeyData globalKey in _iniData.Global)
{
string fieldName = globalKey.KeyName;
if (!classConfigField.ContainsKey(fieldName.ToUpper()))
{
string missingClassFieldComment = "***THE FOLLOWING VALUE IS BEING IGNORED!" + configObj.GetType() + " does not have a field corresponding to " + globalKey.KeyName;
if(!globalKey.Comments.Contains(missingClassFieldComment))
globalKey.Comments.Add(missingClassFieldComment);
Logger.config.Debug($"{configObj.GetType()} does not have global section key {globalKey.KeyName}");
}
}
// Similarly, loop through the other section/key pairings of the INI file and check as well.
foreach (SectionData section in _iniData.Sections)
{
foreach (KeyData key in section.Keys)
{
string fieldName = section.SectionName + "." + key.KeyName;
if (!classConfigField.ContainsKey(fieldName.ToUpper()))
{
string missingClassFieldComment = "***THE FOLLOWING VALUE IS BEING IGNORED! " + configObj.GetType() + " does not have a member corresponding to " + fieldName;
if (!key.Comments.Contains(missingClassFieldComment))
key.Comments.Add(missingClassFieldComment);
Logger.config.Debug($"{configObj.GetType()} not have {section.SectionName} section key {key.KeyName}");
}
}
}
return configObj;
}
internal static void SetFieldValue(FieldInfo fieldInfo, object obj, string str)
{
if (str == null)
{
Logger.config.Debug($"{fieldInfo.Name} doesn't have a configuration value! Keeping existing value {fieldInfo.GetValue(obj)}");
return;
}
switch (Type.GetTypeCode(fieldInfo.FieldType))
{
case TypeCode.String:
fieldInfo.SetValue(obj, str);
break;
case TypeCode.Boolean:
fieldInfo.SetValue(obj, Boolean.Parse(str));
break;
case TypeCode.DateTime:
fieldInfo.SetValue(obj, DateTime.Parse(str));
break;
case TypeCode.Int16:
fieldInfo.SetValue(obj, Int16.Parse(str));
break;
case TypeCode.Int32:
fieldInfo.SetValue(obj, Int32.Parse(str));
break;
case TypeCode.Int64:
fieldInfo.SetValue(obj, Int64.Parse(str));
break;
case TypeCode.Double:
fieldInfo.SetValue(obj, Double.Parse(str));
break;
default:
Logger.config.Debug($"{fieldInfo.FieldType} not supported");
throw new Exception();
}
}
public void Save()
{
Logger.config.Debug($"Saving file {Filename}");
if (!Directory.Exists(Path.GetDirectoryName(Filename)))
Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException());
var parser = new FileIniDataParser();
parser.WriteFile(Filename, _iniData);
HasChanged = false;
}
// This is basically serializing from an object to INI Data
public void Store<T>(T obj)
{
Type configObjType = typeof(T);
// Loop through each field in the config object and set the corresponding
// value in the INI Data object.
// Note if there isn't a corresponding value defined in the INI data object,
// it will add one implicitly by accessing it with the brackets
foreach (FieldInfo field in configObjType.GetFields())
{
Type fieldType = field.FieldType;
// if the field is not a primitive type, loop through its subfields
if (Type.GetTypeCode(fieldType) == TypeCode.Object)
{
FieldInfo[] subFields = fieldType.GetFields();
foreach (FieldInfo subField in subFields)
{
if (Type.GetTypeCode(subField.FieldType) != TypeCode.Object)
_iniData[field.Name][subField.Name] = subField.GetValue(field.GetValue(obj)).ToString();
}
}
else
{
_iniData.Global[field.Name] = field.GetValue(obj).ToString();
}
}
HasChanged = true;
InMemoryChanged = true;
}
public string ReadValue(string section, string key)
{
return _iniData[section][key];
}
public string ReadValue(string key)
{
return _iniData.Global[key];
}
}
}