PageRenderTime 126ms CodeModel.GetById 40ms app.highlight 44ms RepoModel.GetById 36ms app.codeStats 0ms

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