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.

267 lines
10 KiB

  1. using IPA.Logging;
  2. using Newtonsoft.Json;
  3. using Newtonsoft.Json.Linq;
  4. using System;
  5. using System.Collections.Specialized;
  6. using System.Collections.Generic;
  7. using System.ComponentModel;
  8. using System.IO;
  9. using IniParser;
  10. using IniParser.Model;
  11. using System.Reflection;
  12. namespace IPA.Config.ConfigProviders
  13. {
  14. [Config.Type("ini")]
  15. internal class IniConfigProvider : IConfigProvider
  16. {
  17. public static void RegisterConfig()
  18. {
  19. Config.Register<IniConfigProvider>();
  20. }
  21. private IniData _iniData;
  22. // TODO: create a wrapper that allows empty object creation
  23. public dynamic Dynamic => _iniData;
  24. public bool HasChanged { get; private set; }
  25. public bool InMemoryChanged { get; set; }
  26. public DateTime LastModified => File.GetLastWriteTime(Filename);
  27. private string _filename;
  28. public string Filename
  29. {
  30. get => _filename;
  31. set
  32. {
  33. if (_filename != null)
  34. throw new InvalidOperationException("Can only assign to Filename once");
  35. _filename = value;
  36. }
  37. }
  38. // Load file
  39. public void Load()
  40. {
  41. Logger.config.Debug($"Loading file {Filename}");
  42. var fileInfo = new FileInfo(Filename);
  43. if (fileInfo.Exists)
  44. {
  45. try
  46. {
  47. var parser = new FileIniDataParser();
  48. parser.Parser.Configuration.CaseInsensitive = true;
  49. _iniData = parser.ReadFile(fileInfo.FullName);
  50. }
  51. catch (Exception e)
  52. {
  53. Logger.config.Error($"Error parsing INI in file {Filename}; resetting to empty INI");
  54. Logger.config.Error(e);
  55. _iniData = new IniData();
  56. }
  57. }
  58. else
  59. {
  60. Logger.config.Debug($"File {fileInfo.FullName} doesn't exist");
  61. _iniData = new IniData();
  62. }
  63. InMemoryChanged = true;
  64. }
  65. // This is basically trying to deserialize from INI data to a config object
  66. public T Parse<T>()
  67. {
  68. // Create an instance of the config object to return
  69. T configObj = Activator.CreateInstance<T>();
  70. // Get a list of the fields declared in the config object
  71. Type configObjType = typeof(T);
  72. // Create a dictionary to record which fields are found in the class files
  73. Dictionary<string, FieldInfo> classConfigField = new Dictionary<string, FieldInfo>();
  74. // This goes through each field of the class to set values
  75. // if found in the configuration file
  76. foreach (FieldInfo field in configObjType.GetFields())
  77. {
  78. Type fieldType = field.FieldType;
  79. // If thie field is an object, loop through its fields ("subfields")
  80. if (Type.GetTypeCode(fieldType) == TypeCode.Object)
  81. {
  82. // Get the sub object value from the config object
  83. object configObjSubObj = field.GetValue(configObj);
  84. foreach (FieldInfo subField in fieldType.GetFields())
  85. {
  86. // If the INI file has a section/key pair corresponding to the field/subfield,
  87. // set the subfield value and store the field info in dictionary
  88. if (_iniData.Sections.ContainsSection(field.Name) && _iniData[field.Name].ContainsKey(subField.Name))
  89. {
  90. SetFieldValue(subField, configObjSubObj, _iniData[field.Name][subField.Name]);
  91. string fieldName = field.Name + "." + subField.Name;
  92. classConfigField[fieldName.ToUpper()] = subField;
  93. }
  94. else
  95. {
  96. Logger.config.Debug($"{field.Name}.{subField.Name} doesn't have a configuration value! Keeping existing value {subField.GetValue(configObjSubObj)}");
  97. }
  98. }
  99. }
  100. else
  101. {
  102. // If a field in the configuration object isn't itself an object, then it's a primitive type
  103. // declared in the global section of the INI file
  104. if (_iniData.Global.ContainsKey(field.Name))
  105. {
  106. SetFieldValue(field, configObj, _iniData.Global[field.Name]);
  107. }
  108. else
  109. {
  110. Logger.config.Debug($"{field.Name} doesn't have a configuration value! Keeping existing value {field.GetValue(configObj)}");
  111. }
  112. string fieldName = field.Name;
  113. // store field info in dictionary (case insensitive)
  114. classConfigField[fieldName.ToUpper()] = field;
  115. }
  116. }
  117. // Loop through the global section of the INI file and see if any of those keys
  118. // don't correspond to a field in the object class by using dictionary
  119. // If any of them don't correspond to a field in the object class, add a comment to INI file
  120. // mentioning those keys are being ignored
  121. foreach (KeyData globalKey in _iniData.Global)
  122. {
  123. string fieldName = globalKey.KeyName;
  124. if (!classConfigField.ContainsKey(fieldName.ToUpper()))
  125. {
  126. string missingClassFieldComment = "***THE FOLLOWING VALUE IS BEING IGNORED!" + configObj.GetType() + " does not have a field corresponding to " + globalKey.KeyName;
  127. if(!globalKey.Comments.Contains(missingClassFieldComment))
  128. globalKey.Comments.Add(missingClassFieldComment);
  129. Logger.config.Debug($"{configObj.GetType()} does not have global section key {globalKey.KeyName}");
  130. }
  131. }
  132. // Similarly, loop through the other section/key pairings of the INI file and check as well.
  133. foreach (SectionData section in _iniData.Sections)
  134. {
  135. foreach (KeyData key in section.Keys)
  136. {
  137. string fieldName = section.SectionName + "." + key.KeyName;
  138. if (!classConfigField.ContainsKey(fieldName.ToUpper()))
  139. {
  140. string missingClassFieldComment = "***THE FOLLOWING VALUE IS BEING IGNORED! " + configObj.GetType() + " does not have a member corresponding to " + fieldName;
  141. if (!key.Comments.Contains(missingClassFieldComment))
  142. key.Comments.Add(missingClassFieldComment);
  143. Logger.config.Debug($"{configObj.GetType()} not have {section.SectionName} section key {key.KeyName}");
  144. }
  145. }
  146. }
  147. return configObj;
  148. }
  149. internal static void SetFieldValue(FieldInfo fieldInfo, object obj, string str)
  150. {
  151. if (str == null)
  152. {
  153. Logger.config.Debug($"{fieldInfo.Name} doesn't have a configuration value! Keeping existing value {fieldInfo.GetValue(obj)}");
  154. return;
  155. }
  156. switch (Type.GetTypeCode(fieldInfo.FieldType))
  157. {
  158. case TypeCode.String:
  159. fieldInfo.SetValue(obj, str);
  160. break;
  161. case TypeCode.Boolean:
  162. fieldInfo.SetValue(obj, Boolean.Parse(str));
  163. break;
  164. case TypeCode.DateTime:
  165. fieldInfo.SetValue(obj, DateTime.Parse(str));
  166. break;
  167. case TypeCode.Int16:
  168. fieldInfo.SetValue(obj, Int16.Parse(str));
  169. break;
  170. case TypeCode.Int32:
  171. fieldInfo.SetValue(obj, Int32.Parse(str));
  172. break;
  173. case TypeCode.Int64:
  174. fieldInfo.SetValue(obj, Int64.Parse(str));
  175. break;
  176. case TypeCode.Double:
  177. fieldInfo.SetValue(obj, Double.Parse(str));
  178. break;
  179. default:
  180. Logger.config.Debug($"{fieldInfo.FieldType} not supported");
  181. throw new Exception();
  182. }
  183. }
  184. public void Save()
  185. {
  186. Logger.config.Debug($"Saving file {Filename}");
  187. if (!Directory.Exists(Path.GetDirectoryName(Filename)))
  188. Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException());
  189. var parser = new FileIniDataParser();
  190. parser.WriteFile(Filename, _iniData);
  191. HasChanged = false;
  192. }
  193. // This is basically serializing from an object to INI Data
  194. public void Store<T>(T obj)
  195. {
  196. Type configObjType = typeof(T);
  197. // Loop through each field in the config object and set the corresponding
  198. // value in the INI Data object.
  199. // Note if there isn't a corresponding value defined in the INI data object,
  200. // it will add one implicitly by accessing it with the brackets
  201. foreach (FieldInfo field in configObjType.GetFields())
  202. {
  203. Type fieldType = field.FieldType;
  204. // if the field is not a primitive type, loop through its subfields
  205. if (Type.GetTypeCode(fieldType) == TypeCode.Object)
  206. {
  207. FieldInfo[] subFields = fieldType.GetFields();
  208. foreach (FieldInfo subField in subFields)
  209. {
  210. if (Type.GetTypeCode(subField.FieldType) != TypeCode.Object)
  211. _iniData[field.Name][subField.Name] = subField.GetValue(field.GetValue(obj)).ToString();
  212. }
  213. }
  214. else
  215. {
  216. _iniData.Global[field.Name] = field.GetValue(obj).ToString();
  217. }
  218. }
  219. HasChanged = true;
  220. InMemoryChanged = true;
  221. }
  222. public string ReadValue(string section, string key)
  223. {
  224. return _iniData[section][key];
  225. }
  226. public string ReadValue(string key)
  227. {
  228. return _iniData.Global[key];
  229. }
  230. }
  231. }