EasyConfiguration.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.ComponentModel.DataAnnotations;
  7. using System.ComponentModel;
  8. using System.IO;
  9. using System.Reflection;
  10. using EasyDevCore.Common;
  11. using System.Text.Json;
  12. using System.Xml;
  13. using EasyDevCore.Common.Security;
  14. using System.Collections;
  15. using System.Runtime.CompilerServices;
  16. namespace EasyDevCore.Configuration
  17. {
  18. /// <summary>
  19. /// Easy Configuration file
  20. /// </summary>
  21. public class EasyConfiguration : INotifyPropertyChanged, IEnumerable<EasyConfigItem>, IDisposable
  22. {
  23. System.Timers.Timer _AutoSaveTimer;
  24. private bool _DataChanged = false;
  25. /// <summary>
  26. /// Gets or sets a value indicating whether this instance is loading file.
  27. /// </summary>
  28. /// <value>
  29. /// <c>true</c> if this instance is loading file; otherwise, <c>false</c>.
  30. /// </value>
  31. protected bool IsLoadingFile { get; set; } = false;
  32. private void OptionChanged(object sender, PropertyChangedEventArgs e)
  33. {
  34. switch (e.PropertyName)
  35. {
  36. case nameof(Options.AutoSaveDelay):
  37. if (_AutoSaveTimer != null)
  38. {
  39. _AutoSaveTimer.Interval = Options.AutoSaveDelay;
  40. }
  41. break;
  42. case nameof(Options.AutoSave):
  43. if (_AutoSaveTimer == null)
  44. {
  45. _AutoSaveTimer = new System.Timers.Timer(Options.AutoSaveDelay * 1000);
  46. _AutoSaveTimer.Elapsed += AutoSaveElapsed;
  47. _AutoSaveTimer.Start();
  48. }
  49. break;
  50. }
  51. }
  52. /// <summary>
  53. /// Gets the options.
  54. /// </summary>
  55. /// <value>
  56. /// The options.
  57. /// </value>
  58. public EasyConfigOptions Options { get; private set; }
  59. /// <summary>
  60. /// The items
  61. /// </summary>
  62. protected Dictionary<string, EasyConfigItem> Items { get; private set; } = new Dictionary<string, EasyConfigItem>(StringComparer.InvariantCultureIgnoreCase);
  63. /// <summary>
  64. /// Prevents a default instance of the <see cref="EasyConfiguration"/> class from being created.
  65. /// </summary>
  66. /// <exception cref="System.ArgumentNullException"></exception>
  67. /// <exception cref="System.NotImplementedException"></exception>
  68. /// <exception cref="System.NotSupportedException"></exception>
  69. private EasyConfiguration()
  70. {
  71. LoadDefaultConfig();
  72. }
  73. /// <summary>
  74. /// Initializes a new instance of the <see cref="EasyConfiguration" /> class.
  75. /// </summary>
  76. /// <param name="options">The options.</param>
  77. public EasyConfiguration(EasyConfigOptions options) : this()
  78. {
  79. Options = options;
  80. options.PropertyChanged += OptionChanged;
  81. string configFile = Options.FileName;
  82. var ext = Path.GetExtension(Options.FileName).ToLower();
  83. if (Options.ConfigType == EasyConfigFileType.Auto)
  84. {
  85. if (!ext.In(".xml", ".json"))
  86. {
  87. throw new ConfigurationErrorsException("Extension of file must be .xml/.json");
  88. }
  89. Options.ConfigType = ext.Switch(new[] { ".xml", ".json" }, new[] { EasyConfigFileType.Xml, EasyConfigFileType.Json });
  90. }
  91. else
  92. {
  93. if (string.IsNullOrWhiteSpace(ext))
  94. {
  95. ext = Options.ConfigType.Switch(new[] { EasyConfigFileType.Xml, EasyConfigFileType.Json }, new[] { ".xml", ".json" });
  96. configFile += ext;
  97. }
  98. }
  99. Options.FileName = configFile;
  100. }
  101. /// <summary>
  102. /// Initializes a new instance of the <see cref="EasyConfiguration" /> class.
  103. /// </summary>
  104. /// <param name="configFile">The configuration file.</param>
  105. /// <param name="password">The password.</param>
  106. public EasyConfiguration(string configFile, string password = "") : this(new() { FileName = configFile, EncryptPassword = password })
  107. {
  108. }
  109. /// <summary>
  110. /// Loads the default config.
  111. /// </summary>
  112. private void LoadDefaultConfig()
  113. {
  114. PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(p => p.CanWrite && p.CanRead).ToArray();
  115. foreach (PropertyInfo property in properties)
  116. {
  117. EasyConfigItem item = new();
  118. string itemName = property.Name;
  119. Items[itemName] = item;
  120. item.PropertyType = property.PropertyType;
  121. Attribute[] attrs = (Attribute[])property.GetCustomAttributes(typeof(EasyConfigPropertyAttribute), false);
  122. if (attrs.Length > 0)
  123. {
  124. EasyConfigPropertyAttribute configItem = (EasyConfigPropertyAttribute)attrs[0];
  125. item.Property = configItem;
  126. item.Value = configItem.DefaultValue.ChangeType(property.PropertyType);
  127. }
  128. else
  129. {
  130. item.Property = new();
  131. }
  132. }
  133. DataChanged = false;
  134. }
  135. /// <summary>
  136. /// Clears this instance.
  137. /// </summary>
  138. public void Clear()
  139. {
  140. Items.Clear();
  141. LoadDefaultConfig();
  142. }
  143. /// <summary>
  144. /// Gets or sets the encryption handler.
  145. /// </summary>
  146. /// <value>
  147. /// The encryption handler (string: value, bool: true/false - Encrypted/Decrypted).
  148. /// </value>
  149. public Func<string, bool, string> EncryptionHandler { get; set; }
  150. /// <summary>
  151. /// Gets or sets a value indicating whether [data changed].
  152. /// </summary>
  153. /// <value>
  154. /// <c>true</c> if [data changed]; otherwise, <c>false</c>.
  155. /// </value>
  156. public bool DataChanged
  157. {
  158. get
  159. {
  160. return _DataChanged;
  161. }
  162. protected set
  163. {
  164. _DataChanged = value;
  165. if (_AutoSaveTimer != null)
  166. {
  167. if (DataChanged)
  168. {
  169. _AutoSaveTimer.Stop();
  170. _AutoSaveTimer.Start();
  171. }
  172. else
  173. {
  174. _AutoSaveTimer.Stop();
  175. }
  176. }
  177. }
  178. }
  179. /// <summary>
  180. /// Automatics the save elapsed.
  181. /// </summary>
  182. /// <param name="sender">The sender.</param>
  183. /// <param name="e">The <see cref="System.Timers.ElapsedEventArgs"/> instance containing the event data.</param>
  184. private void AutoSaveElapsed(object sender, System.Timers.ElapsedEventArgs e)
  185. {
  186. _AutoSaveTimer.Stop();
  187. lock (this)
  188. {
  189. this.Save();
  190. }
  191. }
  192. /// <summary>
  193. /// Gets or sets the <see cref="System.Object" /> with the specified name.
  194. /// </summary>
  195. /// <value>
  196. /// The <see cref="System.Object" />.
  197. /// </value>
  198. /// <param name="name">The name (case insensitive).</param>
  199. /// <returns></returns>
  200. /// <exception cref="ConfigurationErrorsException"></exception>
  201. /// <exception cref="System.NotSupportedException"></exception>
  202. public object this[string name]
  203. {
  204. get
  205. {
  206. return Items[name].Value;
  207. }
  208. set
  209. {
  210. var item = Items[name];
  211. if (item.Value != value)
  212. {
  213. if ((item.Property.MinValue != null && value.CompareValue(item.Property.MinValue) == 1)
  214. || (item.Property.MaxValue != null && value.CompareValue(item.Property.MaxValue) == 2))
  215. {
  216. var s = string.Format("[{0}] must be {1}{2}{3}", name, (item.Property.MinValue != null ? $">= {item.Property.MinValue}" : ""),
  217. (item.Property.MinValue != null && item.Property.MaxValue != null ? " and " : ""), (item.Property.MaxValue != null ? $"<= {item.Property.MaxValue}" : ""));
  218. throw new ConfigurationErrorsException(s);
  219. }
  220. Items[name].Value = value;
  221. if(!IsLoadingFile)
  222. {
  223. NotifyPropertyChanged(name);
  224. }
  225. }
  226. }
  227. }
  228. /// <summary>
  229. /// Gets the specified name.
  230. /// </summary>
  231. /// <typeparam name="T"></typeparam>
  232. /// <param name="name">The name.</param>
  233. /// <returns></returns>
  234. protected T Get<T>([CallerMemberName] string name = null)
  235. {
  236. return (T)this[name];
  237. }
  238. /// <summary>
  239. /// Sets the specified value.
  240. /// </summary>
  241. /// <param name="value">The value.</param>
  242. /// <param name="name">The name.</param>
  243. protected void Set(object value, [CallerMemberName] string name = null)
  244. {
  245. this[name] = value;
  246. }
  247. /// <summary>
  248. /// Existses the specified name.
  249. /// </summary>
  250. /// <param name="name">The name (case insensitive).</param>
  251. /// <returns></returns>
  252. public bool Exists(string name) => Items.ContainsKey(name);
  253. /// <summary>
  254. /// Gets the content of the file.
  255. /// </summary>
  256. /// <returns></returns>
  257. /// hjnui
  258. private string GetFileContent()
  259. {
  260. return System.IO.File.ReadAllText(Options.FileName);
  261. }
  262. /// <summary>
  263. /// Saves the content of the file.
  264. /// </summary>
  265. private void SaveFileContent(string content)
  266. {
  267. System.IO.File.WriteAllText(Options.FileName, content);
  268. }
  269. /// <summary>
  270. /// Loads the item value.
  271. /// </summary>
  272. /// <param name="itemName">Name of the item.</param>
  273. /// <param name="rawValue">The raw value.</param>
  274. private void LoadItemValue(string itemName, string rawValue)
  275. {
  276. object value = rawValue;
  277. if (Items.ContainsKey(itemName))
  278. {
  279. var item = Items[itemName];
  280. if (item.Property.IsEncrypted)
  281. {
  282. if (EncryptionHandler != null)
  283. {
  284. value = EncryptionHandler(rawValue, false);
  285. }
  286. else
  287. {
  288. value = EncryptionHelper.DecryptString(Options.EncryptPassword, rawValue);
  289. }
  290. if ((string)value == rawValue) //Value not encrypted
  291. {
  292. DataChanged = true;
  293. }
  294. }
  295. if (item.Property.IsSerialization)
  296. {
  297. value = JsonSerializer.Deserialize(((string)value).FromBase64(), item.PropertyType);
  298. }
  299. else
  300. {
  301. value = value.ChangeType(item.PropertyType);
  302. }
  303. this[itemName] = value;
  304. }
  305. else
  306. {
  307. Items.Add(itemName, new() { PropertyType = typeof(string), Value = value, Property = new()});
  308. }
  309. }
  310. /// <summary>
  311. /// Loads the json configuration.
  312. /// </summary>
  313. private void LoadJsonConfig()
  314. {
  315. JsonSerializer.Deserialize<Dictionary<string, object>>(GetFileContent()).ForEach(kv => LoadItemValue(kv.Key, kv.Value == null ? null : kv.Value.ToString()));
  316. }
  317. /// <summary>
  318. /// Loads the XML configuration.
  319. /// </summary>
  320. private void LoadXmlConfig()
  321. {
  322. using (System.Xml.XmlReader reader = XmlReader.Create(new StringReader(GetFileContent())))
  323. {
  324. while (reader.Read() && !(reader.NodeType == XmlNodeType.Element && reader.LocalName.Equals("configuration", StringComparison.InvariantCultureIgnoreCase)))
  325. {
  326. }
  327. while (reader.Read())
  328. {
  329. reader.MoveToContent();
  330. if (reader.NodeType == XmlNodeType.Element)
  331. {
  332. string itemName = reader.Name;
  333. string itemValue = reader.ReadInnerXml();
  334. LoadItemValue(itemName, itemValue);
  335. //data[itemName] = itemValue;
  336. }
  337. }
  338. }
  339. }
  340. /// <summary>
  341. /// Loads this settings
  342. /// </summary>
  343. /// <param name="isAutoCreate">if set to <c>true</c> [is automatic create].</param>
  344. public void Load(bool isAutoCreate = true)
  345. {
  346. if (isAutoCreate)
  347. {
  348. if (!File.Exists(Options.FileName))
  349. {
  350. Save();
  351. }
  352. }
  353. DataChanged = false;
  354. try
  355. {
  356. IsLoadingFile = true;
  357. if (Options.ConfigType == EasyConfigFileType.Json)
  358. {
  359. LoadJsonConfig();
  360. }
  361. if (Options.ConfigType == EasyConfigFileType.Xml)
  362. {
  363. LoadXmlConfig();
  364. }
  365. if (DataChanged) // Has encrypted value
  366. {
  367. Save();
  368. }
  369. }
  370. finally
  371. {
  372. IsLoadingFile = false;
  373. }
  374. }
  375. /// <summary>
  376. /// Cammels the case.
  377. /// </summary>
  378. /// <param name="key">The key.</param>
  379. /// <returns></returns>
  380. private string CammelCase(string key) => key.Substitude(0, key.Substring(0, 1).ToLower());
  381. /// <summary>
  382. /// Gets the save items.
  383. /// </summary>
  384. /// <returns></returns>
  385. private Dictionary<string, string> GetSaveItems()
  386. {
  387. Dictionary<string, string> data = new();
  388. JsonSerializerOptions options = new() { WriteIndented = true };
  389. foreach (var item in Items.Where(it => !it.Value.Property.IsInternal))
  390. {
  391. var itemName = item.Value.Property.Alias ?? CammelCase(item.Key);
  392. var property = item.Value.Property;
  393. var value = string.Empty;
  394. if (property.IsSerialization)
  395. {
  396. value = JsonSerializer.Serialize(item.Value.Value, item.Value.PropertyType, options: options);
  397. }
  398. else
  399. {
  400. value = item.Value.Value.StringFrom();
  401. }
  402. if (property.IsEncrypted)
  403. {
  404. if (EncryptionHandler != null)
  405. {
  406. value = EncryptionHandler(value, true);
  407. }
  408. else
  409. {
  410. value = EncryptionHelper.EncryptString(value, Options.EncryptPassword);
  411. }
  412. }
  413. data[itemName] = value;
  414. }
  415. return data;
  416. }
  417. /// <summary>
  418. /// Saves the json configuration.
  419. /// </summary>
  420. private void SaveJsonConfig()
  421. {
  422. var data = GetSaveItems();
  423. SaveFileContent(JsonSerializer.Serialize(data, data.GetType(), options: new() { WriteIndented = true }));
  424. }
  425. /// <summary>
  426. /// Saves the XML configuration.
  427. /// </summary>
  428. private void SaveXmlConfig()
  429. {
  430. var data = GetSaveItems();
  431. XmlDocument doc = XmlHelper.CreateDocument("configuration");
  432. XmlNode configNode = XmlHelper.SelectSingleNode(doc, "configuration");
  433. foreach(var kp in data)
  434. {
  435. XmlHelper.AddSingleChildNode(configNode, kp.Key, kp.Value);
  436. }
  437. SaveFileContent(XmlHelper.DocToString(doc));
  438. }
  439. /// <summary>
  440. /// Saves this settings
  441. /// </summary>
  442. public virtual void Save()
  443. {
  444. if (Options.ConfigType == EasyConfigFileType.Json)
  445. {
  446. SaveJsonConfig();
  447. }
  448. if (Options.ConfigType == EasyConfigFileType.Xml)
  449. {
  450. SaveXmlConfig();
  451. }
  452. DataChanged = false;
  453. }
  454. #region IEnumerable<EasyConfigurationItem> Members
  455. IEnumerator<EasyConfigItem> IEnumerable<EasyConfigItem>.GetEnumerator()
  456. {
  457. return Items.Values.GetEnumerator();
  458. }
  459. IEnumerator IEnumerable.GetEnumerator()
  460. {
  461. return Items.Values.GetEnumerator();
  462. }
  463. #endregion
  464. #region INotifyPropertyChanged Members
  465. /// <summary>
  466. /// Occurs when a property value changes.
  467. /// </summary>
  468. public event PropertyChangedEventHandler PropertyChanged;
  469. private void NotifyPropertyChanged(String propertyName)
  470. {
  471. DataChanged = true;
  472. OnPropertyChanged(propertyName);
  473. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  474. }
  475. /// <summary>
  476. /// Called when [property changed].
  477. /// </summary>
  478. /// <param name="name">The name.</param>
  479. protected virtual void OnPropertyChanged(string name)
  480. {
  481. }
  482. #endregion
  483. #region IDisposable Members
  484. /// <summary>
  485. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  486. /// </summary>
  487. public void Dispose()
  488. {
  489. Items.Clear();
  490. if (_AutoSaveTimer != null)
  491. {
  492. if (_AutoSaveTimer.Enabled)
  493. {
  494. AutoSaveElapsed(null, null);
  495. }
  496. _AutoSaveTimer.Dispose();
  497. }
  498. }
  499. #endregion
  500. }
  501. }