PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/Source/Tools/ResourceWrapper.Generator/ResourceParser.cs

https://bitbucket.org/EugeneLepekhin/fusion
C# | 336 lines | 300 code | 23 blank | 13 comment | 112 complexity | 09dd18fb621b5345cab40e309729551a MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Text.RegularExpressions;
  7. using System.Xml;
  8. namespace ResourceWrapper.Generator {
  9. /// <summary>
  10. /// Parses and creates list of Items for generator to produce code.
  11. /// Expect the following syntax on comment:
  12. /// - minus in the first position of the comment turn off any parsing and property will be generated.
  13. /// !(value1, value2, ... valueN) list of allowed items. The value of the resource is expected to be one of the value1-valueN
  14. /// If there is no formating parameters than comment is ignored
  15. /// If there are formating parameters comment should declare parameters of formating function: {type1 parameter1, type2 parameter2, ... typeM parameterM}
  16. /// </summary>
  17. internal class ResourceParser {
  18. public static IEnumerable<ResourceItem> Parse(string file, bool enforceParameterDeclaration, IEnumerable<string> satelites) {
  19. XmlDocument resource = new XmlDocument();
  20. resource.Load(file);
  21. XmlNodeList nodeList = ResourceParser.SelectResources(resource);
  22. if(nodeList != null && 0 < nodeList.Count) {
  23. ResourceParser parser = new ResourceParser(file, enforceParameterDeclaration, satelites);
  24. List<ResourceItem> list = new List<ResourceItem>();
  25. Action<ResourceItem> assign = item => { if(item != null) { list.Add(item); } };
  26. parser.Parse(nodeList,
  27. (string name, string value, string comment) => assign(parser.GenerateInclude(name, value, comment)),
  28. (string name, string value, string comment) => assign(parser.GenerateString(name, value, comment))
  29. );
  30. if(parser.errorCount == 0 && parser.satelites != null) {
  31. parser.VerifySatelites(list);
  32. }
  33. if(parser.errorCount == 0) {
  34. return list;
  35. } else {
  36. return null;
  37. }
  38. }
  39. return Enumerable.Empty<ResourceItem>();
  40. }
  41. private static XmlNodeList SelectResources(XmlDocument resource) {
  42. return resource.SelectNodes("/root/data");
  43. }
  44. private string fileName;
  45. private readonly bool enforceParameterDeclaration;
  46. private readonly IEnumerable<string> satelites;
  47. private const RegexOptions regexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline;
  48. private readonly Regex variantList = new Regex(@"^!\((?<list>.*)\)", regexOptions);
  49. // {int index, string message} hello, world {System.Int32 param} comment {} {MyType value1, Other value2, OneMore last}
  50. private readonly Regex functionParameters = new Regex(@"\{(?<param>[^}]+)\}", regexOptions);
  51. // a.b.c.d a, int i, string text, System.Int32 index
  52. private readonly Regex parameterDeclaration = new Regex(@"^(?<type>[A-Za-z_][A-Za-z_0-9]*(\s*\.\s*[A-Za-z_][A-Za-z_0-9]*)*)\s+(?<name>[A-Za-z_][A-Za-z_0-9]*)$", regexOptions);
  53. private int errorCount;
  54. private int warningCount;
  55. private ResourceParser(string file, bool enforceParameterDeclaration, IEnumerable<string> satelites) {
  56. this.fileName = file;
  57. this.enforceParameterDeclaration = enforceParameterDeclaration;
  58. this.satelites = satelites;
  59. this.errorCount = 0;
  60. this.warningCount = 0;
  61. }
  62. private void Parse(XmlNodeList nodeList, Action<string, string, string> generateInclude, Action<string, string, string> generateString) {
  63. foreach(XmlNode node in nodeList) {
  64. XmlAttribute nodeName = node.Attributes["name"];
  65. if(nodeName == null) {
  66. this.Error("Unknown Node", "Resource name is missing");
  67. continue;
  68. }
  69. string name = nodeName.InnerText.Trim();
  70. XmlNode nodeValue = node.SelectSingleNode("value");
  71. if(nodeValue == null) {
  72. this.Error(name, "Value missing");
  73. continue;
  74. }
  75. string value = nodeValue.InnerText.Trim();
  76. XmlNode nodeComment = node.SelectSingleNode("comment");
  77. string comment = (nodeComment != null) ? nodeComment.InnerText.Trim() : string.Empty;
  78. if(node.Attributes["type"] != null) {
  79. generateInclude(name, value, comment);
  80. } else {
  81. generateString(name, value, comment);
  82. }
  83. }
  84. }
  85. private void VerifySatelites(List<ResourceItem> itemList) {
  86. Dictionary<string, ResourceItem> items = new Dictionary<string,ResourceItem>(itemList.Count);
  87. itemList.ForEach(i => items.Add(i.Name, i));
  88. string mainFile = this.fileName;
  89. foreach(string file in this.satelites) {
  90. XmlDocument resource = new XmlDocument();
  91. resource.Load(file);
  92. XmlNodeList nodeList = ResourceParser.SelectResources(resource);
  93. if(nodeList != null && 0 < nodeList.Count) {
  94. this.fileName = file;
  95. this.Parse(nodeList, (a, b, c) => {},
  96. (string name, string value, string comment) => {
  97. ResourceItem item;
  98. if(items.TryGetValue(name, out item)) {
  99. if(!item.SuppressValidation) {
  100. int count = this.ValidateFormatItems(name, value, false);
  101. if(item.Parameters != null) {
  102. if(count != item.Parameters.Count) {
  103. this.Warning(name, "number of parameters is different from the same resource in the main resource file \"{0}\"", mainFile);
  104. }
  105. } else if(item.LocalizationVariants != null) {
  106. if(!item.LocalizationVariants.Contains(value)) {
  107. this.Error(name, "provided value is not in variant list defined in main resource file: \"{0}\"", mainFile);
  108. }
  109. }
  110. }
  111. } else {
  112. this.Warning(name, "resource does not exist in the main resource file \"{0}\"", mainFile);
  113. }
  114. }
  115. );
  116. }
  117. }
  118. this.fileName = mainFile;
  119. }
  120. private bool Error(string nodeName, string errorText, params object[] args) {
  121. //"C:\Projects\TestApp\TestApp\Subfolder\TextMessage.resx(10,1): error URW001: nodeName: my error"
  122. Message.Error("{0}(1,1): error URW001: {1}: {2}", this.fileName, nodeName, this.Format(errorText, args));
  123. this.errorCount++;
  124. return false;
  125. }
  126. private void Warning(string nodeName, string errorText, params object[] args) {
  127. Message.Warning("{0}(1,1): warning: {1}: {2}", this.fileName, nodeName, this.Format(errorText, args));
  128. this.warningCount++;
  129. Message.Flush();
  130. }
  131. private bool Corrupted(string nodeName) {
  132. return this.Error(nodeName, "Structure of the value node is corrupted");
  133. }
  134. private string Format(string format, params object[] args) {
  135. return string.Format(CultureInfo.InvariantCulture, format, args);
  136. }
  137. private ResourceItem GenerateInclude(string name, string value, string comment) {
  138. string[] list = value.Split(';');
  139. if(list.Length < 2) {
  140. this.Corrupted(name);
  141. return null;
  142. }
  143. string file = list[0];
  144. list = list[1].Split(',');
  145. if(list.Length < 2) {
  146. this.Corrupted(name);
  147. return null;
  148. }
  149. string type = list[0].Trim();
  150. if(type == "System.String") {
  151. return this.GenerateString(name, this.Format("content of the file: \"{0}\"", file), comment);
  152. }
  153. return this.GenerateObjectProperty(name, file, type);
  154. }
  155. private ResourceItem GenerateObjectProperty(string name, string file, string type) {
  156. return (0 == this.errorCount) ? new ResourceItem(name, file, type) : null;
  157. }
  158. private ResourceItem GenerateString(string name, string value, string comment) {
  159. ResourceItem item = new ResourceItem(name, value, "string");
  160. if(!comment.StartsWith("-")) {
  161. if(!this.IsVariantList(item, value, comment)) {
  162. this.ParseFormatList(item, value, comment);
  163. }
  164. } else {
  165. item.SuppressValidation = true;
  166. }
  167. return (0 == this.errorCount) ? item : null;
  168. }
  169. private bool IsVariantList(ResourceItem item, string value, string comment) {
  170. Match match = this.variantList.Match(comment);
  171. if(match.Success) {
  172. string listText = match.Groups["list"].Value;
  173. string[] variants = listText.Split(',');
  174. List<string> list = new List<string>();
  175. foreach(string var in variants) {
  176. string text = var.Trim();
  177. if(0 < text.Length) {
  178. list.Add(text);
  179. }
  180. }
  181. item.LocalizationVariants = list;
  182. if(!list.Contains(value)) {
  183. this.Error(item.Name, "Localization variants does not contain provided value: {0}", value);
  184. }
  185. }
  186. return match.Success;
  187. }
  188. private void ParseFormatList(ResourceItem item, string value, string comment) {
  189. int count = this.ValidateFormatItems(item.Name, value, true);
  190. if(0 < count) {
  191. Match paramsMatch = this.functionParameters.Match(comment);
  192. if(paramsMatch.Success) {
  193. string[] list = paramsMatch.Groups["param"].Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
  194. List<Parameter> parameterList = new List<Parameter>(list.Length);
  195. foreach(string text in list) {
  196. if(!string.IsNullOrWhiteSpace(text)) {
  197. Match parameterMatch = this.parameterDeclaration.Match(text.Trim());
  198. if(parameterMatch.Success) {
  199. parameterList.Add(new Parameter(parameterMatch.Groups["type"].Value, parameterMatch.Groups["name"].Value));
  200. } else {
  201. this.Error(item.Name, "bad parameter declaration: {0}", text.Trim());
  202. }
  203. }
  204. }
  205. if(parameterList.Count != count) {
  206. this.Error(item.Name, "number of parameters expected by value of the string do not match to provided parameter list in comment");
  207. }
  208. item.Parameters = parameterList;
  209. } else {
  210. string error = "string value contains formating placeholders, but function parameters declaration is missing in comment.";
  211. if(this.enforceParameterDeclaration) {
  212. this.Error(item.Name, error);
  213. } else {
  214. this.Warning(item.Name, error);
  215. }
  216. }
  217. }
  218. }
  219. private int ValidateFormatItems(string name, string value, bool requareAllParameters) {
  220. Func<int> error = () => {
  221. this.Error(name, "Invalid formating item.");
  222. return -1;
  223. };
  224. HashSet<int> indexes = new HashSet<int>();
  225. for(int i = 0; i < value.Length; i++) {
  226. if('}' == value[i]) {
  227. i++;
  228. if(!(i < value.Length && '}' == value[i])) {
  229. this.Error(name, "Input string is not in correct format");
  230. return -1;
  231. }
  232. } else if('{' == value[i]) {
  233. i++;
  234. if(i < value.Length && '{' == value[i]) {
  235. continue; // skip escaped {
  236. }
  237. // Formating item is started
  238. bool isNumber = false;
  239. int index = 0;
  240. while(i < value.Length && '0' <= value[i] && value[i] <= '9' && index < 1000000) {
  241. index = index * 10 + value[i] - '0';
  242. isNumber = true;
  243. i++;
  244. }
  245. if(!isNumber || 1000000 <= index) {
  246. return error();
  247. }
  248. indexes.Add(index);
  249. while(i < value.Length && ' ' == value[i]) {
  250. i++;
  251. }
  252. if(i < value.Length && ',' == value[i]) {
  253. i++;
  254. while(i < value.Length && ' ' == value[i]) {
  255. i++;
  256. }
  257. if(i < value.Length && '-' == value[i]) {
  258. i++; //skip sign
  259. }
  260. isNumber = false;
  261. index = 0;
  262. while(i < value.Length && '0' <= value[i] && value[i] <= '9' && index < 1000000) {
  263. isNumber = true;
  264. i++;
  265. }
  266. if(!isNumber || 1000000 <= index) {
  267. return error();
  268. }
  269. }
  270. while(i < value.Length && ' ' == value[i]) {
  271. i++;
  272. }
  273. if(i < value.Length && ':' == value[i]) {
  274. // Inside format string. It is allowed to have escaped open and closed braces, so skip them until single }
  275. for(;;) {
  276. i++;
  277. while(i < value.Length && '}' != value[i]) {
  278. if('{' == value[i]) {
  279. i++;
  280. if(!(i < value.Length && '{' == value[i])) {
  281. return error();
  282. }
  283. }
  284. i++;
  285. }
  286. if(i + 1 < value.Length && '}' == value[i + 1]) {
  287. i++;
  288. } else {
  289. break;
  290. }
  291. }
  292. }
  293. if(!(i < value.Length && '}' == value[i])) {
  294. return error();
  295. }
  296. }
  297. }
  298. int current = 0;
  299. foreach(int index in indexes.OrderBy(i => i)) {
  300. if(index != current++) {
  301. if(requareAllParameters) {
  302. this.Error(name, "parameter number {0} is missing in the string", current - 1);
  303. } else {
  304. this.Warning(name, "parameter number {0} is missing in the string", current - 1);
  305. break;
  306. }
  307. return -1; // report just one missing parameter number
  308. }
  309. }
  310. return indexes.Count;
  311. }
  312. }
  313. }