PageRenderTime 46ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/Xtensive.Storage/Xtensive.Storage.Tests/Linq/QueryDumper.cs

https://code.google.com/p/dataobjectsdotnet/
C# | 578 lines | 522 code | 48 blank | 8 comment | 90 complexity | 64e2494cd79c2e400ead2f71d139d824 MD5 | raw file
Possible License(s): AGPL-3.0
  1. // Copyright (C) 2003-2010 Xtensive LLC.
  2. // All rights reserved.
  3. // For conditions of distribution and use, see license.
  4. // Created by: Alexey Gamzov
  5. // Created: 2009.03.27
  6. using System;
  7. using System.Collections;
  8. using System.Collections.Generic;
  9. using System.Diagnostics;
  10. using System.Linq;
  11. using System.Reflection;
  12. using System.Runtime.CompilerServices;
  13. using System.Text;
  14. using System.Xml;
  15. using System.Xml.XPath;
  16. using Xtensive.Core;
  17. using Xtensive.Storage.Linq;
  18. using Xtensive.Core.Reflection;
  19. namespace Xtensive.Storage.Tests.Linq
  20. {
  21. public class QueryDumper
  22. {
  23. private int treeDepth;
  24. private int fillStringLength;
  25. private bool containsEnumerable;
  26. private void DumpInternal(IEnumerable query, bool showResults)
  27. {
  28. EnumerateAll(query);
  29. if (showResults) {
  30. treeDepth = 1;
  31. fillStringLength = 0;
  32. containsEnumerable = false;
  33. var listOfElements = new List<object>();
  34. foreach (var o in query)
  35. listOfElements.Add(o);
  36. if (listOfElements.Count==0) {
  37. Log.Info("NULL");
  38. return;
  39. }
  40. try {
  41. var document = new XmlDocument();
  42. CreateNodeTree(listOfElements, ref document, "Root", document);
  43. if (!containsEnumerable) {
  44. CorrectNodeTree(ref document);
  45. var str = new StringBuilder("|");
  46. foreach (XmlNode o in document.DocumentElement.ChildNodes[0].ChildNodes) {
  47. var value = Int32.Parse(o.Attributes["length"].Value);
  48. fillStringLength = fillStringLength + value;
  49. str.Append(" ").Append(o.Name).Append(CreateFillString(value - o.Name.Length - 3, ' ')).Append("|");
  50. }
  51. var groupHeader = new List<string>();
  52. groupHeader.Add(CreateFillString(fillStringLength + 1, '*'));
  53. groupHeader.Add(str.ToString());
  54. groupHeader.Add(CreateFillString(fillStringLength + 1, '*'));
  55. if (document.DocumentElement.ChildNodes[0].Attributes["group"]==null) {
  56. foreach (var s in groupHeader)
  57. Log.Info(s);
  58. OutputLog(document, null);
  59. }
  60. else
  61. OutputLog(document, groupHeader);
  62. }
  63. else {
  64. Log.Info("Correct output is impossible.");
  65. // EnumerateAll(query);
  66. }
  67. }
  68. catch {
  69. Log.Info("Errors occurred during execution.");
  70. // EnumerateAll(query);
  71. }
  72. }
  73. }
  74. public static void Dump(object value)
  75. {
  76. Dump(value, false);
  77. }
  78. public static void Dump(object value, bool showResults)
  79. {
  80. if (value is IEnumerable)
  81. Dump((IEnumerable) value, showResults);
  82. else {
  83. try {
  84. Dump(new[] {value}, showResults);
  85. }
  86. catch {
  87. Log.Info(value==null ? "NULL" : value.ToString());
  88. }
  89. }
  90. }
  91. public static void Dump(IEnumerable value)
  92. {
  93. Dump(value, false);
  94. }
  95. public static void Dump(IEnumerable value, bool showResults)
  96. {
  97. var dumper = new QueryDumper();
  98. dumper.DumpInternal(value, showResults);
  99. }
  100. #region Private Length related methods
  101. private void FillLength(ref XmlDocument document, XmlNode rootNode)
  102. {
  103. foreach (XmlNode node in rootNode.ChildNodes) {
  104. if (!node.HasChildNodes) {
  105. if (node.Attributes["value"]!=null) {
  106. var l = node.Attributes["value"].Value.Length + 5;
  107. if (l > Int32.Parse(node.Attributes["length"].Value) + 5)
  108. node.Attributes["length"].Value = l.ToString();
  109. else
  110. node.Attributes["length"].Value = (Int32.Parse(node.Attributes["length"].Value) + 5).ToString();
  111. }
  112. }
  113. else
  114. FillLength(ref document, node);
  115. }
  116. }
  117. private void FillLengths(ref XmlDocument document, XmlNodeList nodes)
  118. {
  119. foreach (XmlNode node in nodes)
  120. FillLength(ref document, node);
  121. }
  122. private void CorrectMaxLengths(ref XmlDocument document, XmlNodeList nodes)
  123. {
  124. var firstNodes = nodes;
  125. if (nodes[0].HasChildNodes && nodes[0].ParentNode==document.DocumentElement)
  126. firstNodes = nodes[0].ChildNodes;
  127. foreach (XmlNode node in firstNodes) {
  128. if (!node.HasChildNodes) {
  129. if (node.Attributes["length"]!=null) {
  130. int currentLength = Int32.Parse(node.Attributes["length"].Value);
  131. var resultNodes = document.SelectNodes("//" + node.Name);
  132. foreach (XmlNode o in resultNodes) {
  133. var attribute = Int32.Parse(o.Attributes["length"].Value);
  134. if (attribute > currentLength)
  135. currentLength = attribute;
  136. }
  137. foreach (XmlNode o in resultNodes)
  138. o.Attributes["length"].Value = currentLength.ToString();
  139. }
  140. }
  141. else {
  142. CorrectMaxLengths(ref document, node.ChildNodes);
  143. }
  144. }
  145. }
  146. private int FillMaxLengths(ref XmlDocument document, XmlNodeList nodes)
  147. {
  148. var sum = 0;
  149. var correctNodes = nodes;
  150. if (nodes[0].HasChildNodes && nodes[0].ParentNode==document.DocumentElement)
  151. correctNodes = nodes[0].ChildNodes;
  152. foreach (XmlNode o in correctNodes) {
  153. if (!o.HasChildNodes)
  154. sum += Int32.Parse(o.Attributes["length"].Value);
  155. else
  156. sum += FillMaxLengths(ref document, o.ChildNodes);
  157. }
  158. if (correctNodes[0].ParentNode.ParentNode!=document.DocumentElement) {
  159. var maxLength = Int32.Parse(correctNodes[0].ParentNode.Attributes["length"].Value);
  160. if (maxLength < sum)
  161. correctNodes[0].ParentNode.Attributes["length"].Value = sum.ToString();
  162. }
  163. return sum;
  164. }
  165. private void FillCorrectMaxLengths(ref XmlDocument document, XmlNodeList nodes)
  166. {
  167. var correctNodes = nodes;
  168. if (nodes[0].HasChildNodes && nodes[0].ParentNode==document.DocumentElement)
  169. correctNodes = nodes[0].ChildNodes;
  170. foreach (XmlNode node in correctNodes) {
  171. var allNodes = document.SelectNodes("//" + node.Name);
  172. foreach (XmlNode o in allNodes)
  173. o.Attributes["length"].Value = node.Attributes["length"].Value;
  174. if (node.HasChildNodes)
  175. FillCorrectMaxLengths(ref document, node.ChildNodes);
  176. }
  177. }
  178. #endregion
  179. private void CreateNodeTree(List<object> values, ref XmlDocument document, string rootElement, XmlNode parentElement)
  180. {
  181. var parentNode = document.CreateElement(rootElement);
  182. parentElement.AppendChild(parentNode);
  183. if (rootElement!="Root") {
  184. containsEnumerable = true;
  185. var lengthAttribute = document.CreateAttribute("length");
  186. var depthAttribute = document.CreateAttribute("depth");
  187. var valueAttribute = document.CreateAttribute("value");
  188. var enumerableAttribute = document.CreateAttribute("enumerable");
  189. lengthAttribute.Value = rootElement.Length.ToString();
  190. depthAttribute.Value = treeDepth.ToString();
  191. valueAttribute.Value = "NULL";
  192. enumerableAttribute.Value = "TRUE";
  193. parentNode.Attributes.Append(lengthAttribute);
  194. parentNode.Attributes.Append(depthAttribute);
  195. parentNode.Attributes.Append(valueAttribute);
  196. parentNode.Attributes.Append(enumerableAttribute);
  197. }
  198. int groupIndex = 1;
  199. int itemIndex = 0;
  200. foreach (var value in values) {
  201. treeDepth = 1;
  202. var depth = 1;
  203. XmlNode itemNode = document.CreateElement("Item" + itemIndex);
  204. if (value==null || !value.GetType().IsGenericType || (value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition()!=typeof (Grouping<,>))) {
  205. itemNode = document.CreateElement("Item" + itemIndex);
  206. itemIndex++;
  207. parentNode.AppendChild(itemNode);
  208. }
  209. if (value==null || ((GetMemberType(value.GetType())==MemberType.Primitive
  210. || GetMemberType(value.GetType())==MemberType.Unknown) && !value.GetType().IsGenericType)
  211. || (value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition()!=typeof (Grouping<,>))
  212. && (GetMemberType(value.GetType())==MemberType.Primitive
  213. || GetMemberType(value.GetType())==MemberType.Unknown))
  214. depth = AddNode(value, null, ref document, itemNode, depth);
  215. else if (value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition()==typeof (Grouping<,>)) {
  216. var exactValue = (IEnumerable) value;
  217. foreach (var val in exactValue) {
  218. itemNode = document.CreateElement("Item" + itemIndex);
  219. parentNode.AppendChild(itemNode);
  220. itemIndex++;
  221. var groupAttribute = document.CreateAttribute("group");
  222. groupAttribute.Value = groupIndex.ToString();
  223. var keyAttribute = document.CreateAttribute("key");
  224. keyAttribute.Value = KeyToString(value.GetType().GetProperty(WellKnown.KeyFieldName).GetValue(value, null));
  225. itemNode.Attributes.Append(groupAttribute);
  226. itemNode.Attributes.Append(keyAttribute);
  227. var properties = val.GetType().GetProperties();
  228. foreach (var info in properties)
  229. depth = AddNode(val, info, ref document, itemNode, depth);
  230. }
  231. groupIndex++;
  232. }
  233. else {
  234. var properties = value.GetType().GetProperties();
  235. foreach (var info in properties)
  236. depth = AddNode(value, info, ref document, itemNode, depth);
  237. }
  238. }
  239. }
  240. private void CorrectNodeTree(ref XmlDocument document)
  241. {
  242. FillLengths(ref document, document.DocumentElement.ChildNodes);
  243. CorrectMaxLengths(ref document, document.DocumentElement.ChildNodes);
  244. FillMaxLengths(ref document, document.DocumentElement.ChildNodes);
  245. FillCorrectMaxLengths(ref document, document.DocumentElement.ChildNodes);
  246. }
  247. private int AddNode(object value, PropertyInfo property, ref XmlDocument document, XmlNode parentNode, int depthValue)
  248. {
  249. var depth = depthValue;
  250. //One column of primitive type
  251. if (property==null) {
  252. var propertyName = "CompilationResult";
  253. XmlElement node = document.CreateElement(propertyName);
  254. var valueAttribute = document.CreateAttribute("value");
  255. var lengthAttribute = document.CreateAttribute("length");
  256. var depthAttribute = document.CreateAttribute("depth");
  257. lengthAttribute.Value = propertyName.Length.ToString();
  258. depthAttribute.Value = depth.ToString();
  259. node.Attributes.Append(lengthAttribute);
  260. node.Attributes.Append(depthAttribute);
  261. parentNode.AppendChild(node);
  262. valueAttribute.Value = ReplaceTabs(value);
  263. node.Attributes.Append(valueAttribute);
  264. }
  265. else {
  266. if (property.PropertyType.IsGenericType &&
  267. (property.PropertyType.GetGenericTypeDefinition()==typeof (IQueryable<>)
  268. || (property.PropertyType.GetGenericTypeDefinition()==typeof (IEnumerable<>)))) {
  269. var enumerable = (IEnumerable) property.GetValue(value, property.GetIndexParameters());
  270. var list = new List<object>();
  271. foreach (var o in enumerable)
  272. list.Add(o);
  273. CreateNodeTree(list, ref document, property.Name, parentNode);
  274. }
  275. else {
  276. var memberType = GetMemberType(property.PropertyType);
  277. XmlElement node = document.CreateElement(property.Name);
  278. var valueAttribute = document.CreateAttribute("value");
  279. var lengthAttribute = document.CreateAttribute("length");
  280. var depthAttribute = document.CreateAttribute("depth");
  281. lengthAttribute.Value = property.Name.Length.ToString();
  282. depthAttribute.Value = depth.ToString();
  283. node.Attributes.Append(lengthAttribute);
  284. node.Attributes.Append(depthAttribute);
  285. switch (memberType) {
  286. case MemberType.Unknown:
  287. if (ValueIsForOutput(property)) {
  288. parentNode.AppendChild(node);
  289. valueAttribute.Value = ReplaceTabs(property.GetValue(value, null));
  290. node.Attributes.Append(valueAttribute);
  291. node.Attributes.Append(valueAttribute);
  292. }
  293. break;
  294. case MemberType.Entity:
  295. parentNode.AppendChild(node);
  296. var entityValue = property.GetValue(value, null);
  297. valueAttribute.Value = (entityValue!=null) ? entityValue.GetType().GetProperty(WellKnown.KeyFieldName).GetValue(entityValue, null).ToString() : "NULL";
  298. node.Attributes.Append(valueAttribute);
  299. break;
  300. case MemberType.Structure:
  301. case MemberType.Anonymous:
  302. if (depth==treeDepth)
  303. treeDepth += 1;
  304. depth += 1;
  305. parentNode.AppendChild(node);
  306. var itemValue = property.GetValue(value, null);
  307. var itemProperties = itemValue.GetType().GetProperties();
  308. foreach (var itemProperty in itemProperties)
  309. AddNode(itemValue, itemProperty, ref document, node, depth);
  310. depth -= 1;
  311. break;
  312. }
  313. }
  314. }
  315. return depth;
  316. }
  317. private void OutputLog(XmlDocument document, List<string> groupHeader)
  318. {
  319. int currentGroup = 1;
  320. if (groupHeader!=null) {
  321. Log.Info(String.Empty);
  322. Log.Info(CreateFillString(fillStringLength + 1, '*'));
  323. Log.Info("| Key = " + document.DocumentElement.ChildNodes[0].Attributes["key"].Value);
  324. foreach (var s in groupHeader)
  325. Log.Info(s);
  326. }
  327. foreach (XmlNode node in document.DocumentElement.ChildNodes) {
  328. if (groupHeader!=null && Int32.Parse(node.Attributes["group"].Value) > currentGroup) {
  329. Log.Info(String.Empty);
  330. Log.Info(CreateFillString(fillStringLength + 1, '*'));
  331. Log.Info("| Key = " + node.Attributes["key"].Value);
  332. foreach (var s in groupHeader)
  333. Log.Info(s);
  334. currentGroup++;
  335. }
  336. var nodes = node.Clone().SelectNodes("//*[@depth=1]");
  337. var helpList = new List<Pair<XmlNode, int>>();
  338. var drawList = new List<Pair<XmlNode, int>>();
  339. for (int i = 0; i < nodes.Count; i++) {
  340. if (!nodes[i].HasChildNodes)
  341. helpList.Add(new Pair<XmlNode, int>(nodes[i], 0));
  342. else {
  343. var length = 0;
  344. for (int j = i - 1; j >= 0; j = j - 1) {
  345. if (nodes[j].HasChildNodes)
  346. break;
  347. length += Int32.Parse(nodes[j].Attributes["length"].Value);
  348. }
  349. helpList.Add(new Pair<XmlNode, int>(nodes[i], length));
  350. }
  351. drawList.Add(new Pair<XmlNode, int>(nodes[i], 0));
  352. }
  353. DrawSingleLine(drawList);
  354. for (int i = 2; i <= treeDepth; i++) {
  355. drawList.Clear();
  356. nodes = node.Clone().SelectNodes("//*[@depth=" + i + "]");
  357. for (int j = 0; j < nodes.Count; j++) {
  358. var val = helpList.Where(v => v.First.InnerXml==nodes[j].ParentNode.InnerXml);
  359. var value = val!=null ? val.First() : new Pair<XmlNode, int>(null, 0);
  360. if (((j > 0) && nodes[j - 1].ParentNode.InnerXml!=value.First.InnerXml) || j==0)
  361. drawList.Add(new Pair<XmlNode, int>(nodes[j], value.Second));
  362. else if ((j > 0) && nodes[j - 1].ParentNode.InnerXml==value.First.InnerXml)
  363. drawList.Add(new Pair<XmlNode, int>(nodes[j], 0));
  364. }
  365. DrawSingleLine(drawList);
  366. if (i==treeDepth)
  367. break;
  368. helpList.Clear();
  369. for (int l = 0; l < nodes.Count; l++) {
  370. if (!nodes[l].HasChildNodes)
  371. helpList.Add(new Pair<XmlNode, int>(nodes[l], drawList[l].Second));
  372. else {
  373. var length = 0;
  374. for (int j = l - 1; j >= 0; j = j - 1) {
  375. if (nodes[j].HasChildNodes)
  376. break;
  377. length += drawList[j].Second + Int32.Parse(drawList[j].First.Attributes["length"].Value);
  378. }
  379. helpList.Add(new Pair<XmlNode, int>(nodes[l], length + drawList[l].Second));
  380. }
  381. }
  382. }
  383. Log.Info(CreateFillString(fillStringLength + 1, '='));
  384. }
  385. }
  386. private void DrawSingleLine(List<Pair<XmlNode, int>> listOfNodes)
  387. {
  388. var str = new StringBuilder("|");
  389. var separateLine = new StringBuilder("|");
  390. bool drawSeporateLine = false;
  391. foreach (var node in listOfNodes) {
  392. if (!node.First.HasChildNodes) {
  393. var partStr = new StringBuilder();
  394. partStr.Append(CreateFillString(node.Second!=0 ? node.Second - 1 : 0, ' '))
  395. .Append(node.Second!=0 ? "| " : " ")
  396. .Append(node.First.Attributes["value"].Value).Append(CreateFillString(Int32.Parse
  397. (node.First.Attributes["length"].Value) - node.First.Attributes["value"].Value.Length - 3, ' ')).Append("|");
  398. separateLine.Append(CreateFillString(partStr.Length, ' '));
  399. str.Append(partStr);
  400. }
  401. else {
  402. var partStr = new StringBuilder();
  403. drawSeporateLine = true;
  404. partStr.Append(CreateFillString(node.Second, ' '));
  405. foreach (XmlNode o in node.First.ChildNodes) {
  406. partStr.Append(" ").Append(o.Name).Append(CreateFillString(Int32.Parse
  407. (o.Attributes["length"].Value) - o.Name.Length - 3, ' ')).Append("|");
  408. }
  409. separateLine.Append(CreateFillString(partStr.Length, '='));
  410. str.Append(partStr);
  411. }
  412. }
  413. if (str.Length < fillStringLength + 1)
  414. str.Append(CreateFillString(fillStringLength - str.Length, ' ')).Append("|");
  415. if (separateLine.Length < fillStringLength + 1)
  416. separateLine.Append(CreateFillString(fillStringLength - separateLine.Length, ' ')).Append("|");
  417. if (str.ToString().EndsWith(" "))
  418. str.Replace(" ", "|", str.Length - 1, 1);
  419. if (separateLine.ToString().EndsWith(" "))
  420. separateLine.Replace(" ", "|", str.Length - 1, 1);
  421. Log.Info(str.ToString());
  422. if (drawSeporateLine)
  423. Log.Info(separateLine.ToString());
  424. }
  425. private MemberType GetMemberType(Type type)
  426. {
  427. if (typeof (Key).IsAssignableFrom(type))
  428. return MemberType.Key;
  429. if (typeof (IEntity).IsAssignableFrom(type))
  430. return MemberType.Entity;
  431. if (typeof (Structure).IsAssignableFrom(type))
  432. return MemberType.Structure;
  433. if (typeof (EntitySetBase).IsAssignableFrom(type))
  434. return MemberType.EntitySet;
  435. if (Attribute.IsDefined(type, typeof (CompilerGeneratedAttribute), false)
  436. && type.BaseType==typeof (object)
  437. && type.Name.Contains("AnonymousType")
  438. && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
  439. && (type.Attributes & TypeAttributes.NotPublic)==TypeAttributes.NotPublic)
  440. return MemberType.Anonymous;
  441. return MemberType.Unknown;
  442. }
  443. private string ReplaceTabs(object value)
  444. {
  445. if (value==null)
  446. return "NULL";
  447. if (value.GetType()==typeof (byte[]))
  448. return "ByteArray";
  449. return value.ToString().Replace("-", " ").Replace("\n", string.Empty)
  450. .Replace("\t", string.Empty).Replace("\\", string.Empty)
  451. .Replace("\"", " ").Replace("\r", " ").Trim();
  452. }
  453. private string CreateFillString(int count, char element)
  454. {
  455. var list = Enumerable.Repeat(element, count);
  456. string result = String.Empty;
  457. foreach (var item in list)
  458. result = result + item;
  459. return result;
  460. }
  461. private bool ValueIsForOutput(PropertyInfo info)
  462. {
  463. var memberType = GetMemberType(info.PropertyType);
  464. return ((!info.CanWrite && info.PropertyType!=typeof (bool)
  465. && memberType!=MemberType.Key
  466. && info.PropertyType!=typeof (PersistenceState))
  467. || (info.CanWrite)) && info.DeclaringType!=typeof (Persistent)
  468. && info.DeclaringType!=typeof (SessionBound)
  469. && memberType!=MemberType.EntitySet;
  470. }
  471. private string KeyToString(object key)
  472. {
  473. var properties = key.GetType().GetProperties();
  474. var str = new StringBuilder("{ ");
  475. switch (GetMemberType(key.GetType())) {
  476. case MemberType.Structure:
  477. case MemberType.Anonymous:
  478. foreach (var info in properties) {
  479. if (ValueIsForOutput(info)) {
  480. str.Append(info.Name).Append(" = ");
  481. var propertyValue = info.GetValue(key, null);
  482. if (GetMemberType(info.PropertyType)==MemberType.Entity)
  483. str.Append((propertyValue!=null) ? info.PropertyType.GetProperty(WellKnown.KeyFieldName).GetValue(propertyValue, null).ToString() : "NULL");
  484. if (GetMemberType(info.PropertyType)==MemberType.Structure || GetMemberType(info.PropertyType)==MemberType.Anonymous)
  485. str.Append(ReplaceTabs(KeyToString(info.GetValue(key, null))));
  486. else
  487. str.Append(ReplaceTabs(propertyValue))
  488. .Append(" , ");
  489. }
  490. }
  491. break;
  492. case MemberType.Entity:
  493. str.Append(key.GetType().GetProperty(WellKnown.KeyFieldName).GetValue(key, null).ToString());
  494. break;
  495. default:
  496. str.Append(ReplaceTabs(key));
  497. break;
  498. }
  499. if (str[str.Length - 2]==',')
  500. str.Remove(str.Length - 2, 2);
  501. return str.Append(" }").ToString();
  502. }
  503. private static void EnumerateAll(IEnumerable enumerable)
  504. {
  505. foreach (var o in enumerable)
  506. if (o!=null) {
  507. if (o.GetType().IsGenericType && (o.GetType().GetGenericTypeDefinition()==typeof (IQueryable<>)
  508. || o.GetType().GetGenericTypeDefinition()==typeof (IEnumerable<>)
  509. || o.GetType().GetGenericTypeDefinition()==typeof (SubQuery<>)
  510. || o.GetType().GetGenericTypeDefinition()==typeof (Grouping<,>)))
  511. EnumerateAll((IEnumerable) o);
  512. var properties = o.GetType().GetProperties();
  513. foreach (var info in properties) {
  514. if (info.PropertyType.IsGenericType &&
  515. (info.PropertyType.GetGenericTypeDefinition()==typeof (IQueryable<>)
  516. || info.PropertyType.GetGenericTypeDefinition()==typeof (IEnumerable<>)
  517. || info.PropertyType.GetGenericTypeDefinition()==typeof (SubQuery<>)
  518. || info.PropertyType.GetGenericTypeDefinition()==typeof (Grouping<,>)
  519. ))
  520. EnumerateAll((IEnumerable) info.GetValue(o, null));
  521. }
  522. }
  523. }
  524. }
  525. }