Mercurial > pub > ImplabNet
comparison Implab/Xml/JsonXmlReader.cs @ 227:8d5de4eb9c2c v2
Reimplemented JsonXmlReader, added support for null values: JSON null values are
mapped to empty nodes with 'xsi:nil' attribute set to 'true'
author | cin |
---|---|
date | Sat, 09 Sep 2017 03:53:13 +0300 |
parents | |
children | 6fa235c5a760 |
comparison
equal
deleted
inserted
replaced
226:9428ea36838e | 227:8d5de4eb9c2c |
---|---|
1 using Implab.Formats.JSON; | |
2 using System; | |
3 using System.Collections.Generic; | |
4 using System.Globalization; | |
5 using System.Linq; | |
6 using System.Text; | |
7 using System.Threading.Tasks; | |
8 using System.Xml; | |
9 | |
10 namespace Implab.Xml { | |
11 public class JsonXmlReader : XmlReader { | |
12 struct JsonContext { | |
13 public string localName; | |
14 public bool skip; | |
15 } | |
16 | |
17 JSONParser m_parser; | |
18 JsonXmlReaderOptions m_options; | |
19 JsonXmlReaderPosition m_position = JsonXmlReaderPosition.Initial; | |
20 XmlNameTable m_nameTable; | |
21 | |
22 readonly string m_jsonRootName; | |
23 readonly string m_jsonNamespace; | |
24 readonly string m_jsonPrefix; | |
25 readonly bool m_jsonFlattenArrays; | |
26 readonly string m_jsonArrayItemName; | |
27 | |
28 string m_jsonLocalName; | |
29 string m_jsonValueName; | |
30 bool m_jsonSkip; // indicates wheather to generate closing tag for objects or arrays | |
31 | |
32 readonly Stack<JsonContext> m_jsonNameStack = new Stack<JsonContext>(); | |
33 | |
34 XmlQualifiedName m_elementQName; | |
35 string m_elementPrefix; | |
36 int m_elementDepth; | |
37 bool m_elementIsEmpty; | |
38 | |
39 XmlQualifiedName m_qName; | |
40 string m_prefix; | |
41 int m_xmlDepth; | |
42 | |
43 XmlSimpleAttribute[] m_attributes; | |
44 object m_value; | |
45 bool m_isEmpty; | |
46 | |
47 XmlNodeType m_nodeType = XmlNodeType.None; | |
48 | |
49 bool m_isAttribute; // indicates that we are reading attribute nodes | |
50 int m_currentAttribute; | |
51 bool m_currentAttributeRead; | |
52 | |
53 | |
54 XmlNameContext m_context; | |
55 int m_nextPrefix = 1; | |
56 | |
57 readonly string m_xmlnsPrefix; | |
58 readonly string m_xmlnsNamespace; | |
59 readonly string m_xsiPrefix; | |
60 readonly string m_xsiNamespace; | |
61 | |
62 | |
63 public JsonXmlReader(JSONParser parser, JsonXmlReaderOptions options) { | |
64 Safe.ArgumentNotNull(parser, nameof(parser)); | |
65 m_parser = parser; | |
66 | |
67 m_options = options ?? new JsonXmlReaderOptions(); | |
68 | |
69 m_jsonFlattenArrays = m_options.FlattenArrays; | |
70 m_nameTable = m_options.NameTable ?? new NameTable(); | |
71 | |
72 m_jsonRootName = m_nameTable.Add(string.IsNullOrEmpty(m_options.RootName) ? "data" : m_options.RootName); | |
73 m_jsonArrayItemName = m_nameTable.Add(string.IsNullOrEmpty(m_options.ArrayItemName) ? "item" : m_options.ArrayItemName); | |
74 m_jsonNamespace = m_nameTable.Add(m_options.NamespaceUri ?? string.Empty); | |
75 m_jsonPrefix = m_nameTable.Add(m_options.NodesPrefix ?? string.Empty); | |
76 m_xmlnsPrefix = m_nameTable.Add(XmlNameContext.XmlnsPrefix); | |
77 m_xmlnsNamespace = m_nameTable.Add(XmlNameContext.XmlnsNamespace); | |
78 m_xsiPrefix = m_nameTable.Add(XmlNameContext.XsiPrefix); | |
79 m_xsiNamespace = m_nameTable.Add(XmlNameContext.XsiNamespace); | |
80 | |
81 // TODO validate m_jsonRootName, m_jsonArrayItemName | |
82 | |
83 m_context = new XmlNameContext(null); | |
84 } | |
85 | |
86 public override int AttributeCount { | |
87 get { | |
88 return m_attributes == null ? 0 : m_attributes.Length; | |
89 } | |
90 } | |
91 | |
92 public override string BaseURI { | |
93 get { | |
94 return string.Empty; | |
95 } | |
96 } | |
97 | |
98 public override int Depth { | |
99 get { | |
100 return m_xmlDepth; | |
101 } | |
102 } | |
103 | |
104 public override bool EOF { | |
105 get { | |
106 return m_position == JsonXmlReaderPosition.Eof; | |
107 } | |
108 } | |
109 | |
110 public override bool IsEmptyElement { | |
111 get { return m_isEmpty; } | |
112 } | |
113 | |
114 | |
115 public override string LocalName { | |
116 get { | |
117 return m_qName.Name; | |
118 } | |
119 } | |
120 | |
121 public override string NamespaceURI { | |
122 get { | |
123 return m_qName.Namespace; | |
124 } | |
125 } | |
126 | |
127 public override XmlNameTable NameTable { | |
128 get { | |
129 return m_nameTable; | |
130 } | |
131 } | |
132 | |
133 public override XmlNodeType NodeType { | |
134 get { | |
135 return m_nodeType; | |
136 } | |
137 } | |
138 | |
139 public override string Prefix { | |
140 get { | |
141 return m_prefix; | |
142 } | |
143 } | |
144 | |
145 public override ReadState ReadState { | |
146 get { | |
147 switch (m_position) { | |
148 case JsonXmlReaderPosition.Initial: | |
149 return ReadState.Initial; | |
150 case JsonXmlReaderPosition.Eof: | |
151 return ReadState.EndOfFile; | |
152 case JsonXmlReaderPosition.Closed: | |
153 return ReadState.Closed; | |
154 case JsonXmlReaderPosition.Error: | |
155 return ReadState.Error; | |
156 default: | |
157 return ReadState.Interactive; | |
158 }; | |
159 } | |
160 } | |
161 | |
162 public override string Value { | |
163 get { | |
164 return ConvertValueToString(m_value); | |
165 } | |
166 } | |
167 | |
168 static string ConvertValueToString(object value) { | |
169 if (value == null) | |
170 return string.Empty; | |
171 | |
172 switch (Convert.GetTypeCode(value)) { | |
173 case TypeCode.Double: | |
174 return ((double)value).ToString(CultureInfo.InvariantCulture); | |
175 case TypeCode.String: | |
176 return (string)value; | |
177 case TypeCode.Boolean: | |
178 return (bool)value ? "true" : "false"; | |
179 default: | |
180 return value.ToString(); | |
181 } | |
182 } | |
183 | |
184 public override string GetAttribute(int i) { | |
185 Safe.ArgumentInRange(i, 0, AttributeCount - 1, nameof(i)); | |
186 return ConvertValueToString(m_attributes[i].Value); | |
187 } | |
188 | |
189 public override string GetAttribute(string name) { | |
190 if (m_attributes == null) | |
191 return null; | |
192 var qName = m_context.Resolve(name); | |
193 var attr = Array.Find(m_attributes, x => x.QName == qName); | |
194 var value = ConvertValueToString(attr?.Value); | |
195 return value == string.Empty ? null : value; | |
196 } | |
197 | |
198 public override string GetAttribute(string name, string namespaceURI) { | |
199 if (m_attributes == null) | |
200 return null; | |
201 var qName = new XmlQualifiedName(name, namespaceURI); | |
202 var attr = Array.Find(m_attributes, x => x.QName == qName); | |
203 var value = ConvertValueToString(attr?.Value); | |
204 return value == string.Empty ? null : value; | |
205 } | |
206 | |
207 public override string LookupNamespace(string prefix) { | |
208 return m_context.ResolvePrefix(prefix); | |
209 } | |
210 | |
211 public override bool MoveToAttribute(string name) { | |
212 if (m_attributes == null || m_attributes.Length == 0) | |
213 return false; | |
214 | |
215 var qName = m_context.Resolve(name); | |
216 var index = Array.FindIndex(m_attributes, x => x.QName == qName); | |
217 if (index >= 0) { | |
218 MoveToAttributeImpl(index); | |
219 return true; | |
220 } | |
221 return false; | |
222 } | |
223 | |
224 public override bool MoveToAttribute(string name, string ns) { | |
225 if (m_attributes == null || m_attributes.Length == 0) | |
226 return false; | |
227 | |
228 var qName = m_context.Resolve(name); | |
229 var index = Array.FindIndex(m_attributes, x => x.QName == qName); | |
230 if (index >= 0) { | |
231 MoveToAttributeImpl(index); | |
232 return true; | |
233 } | |
234 return false; | |
235 } | |
236 | |
237 void MoveToAttributeImpl(int i) { | |
238 if (!m_isAttribute) { | |
239 m_elementQName = m_qName; | |
240 m_elementDepth = m_xmlDepth; | |
241 m_elementPrefix = m_prefix; | |
242 m_elementIsEmpty = m_isEmpty; | |
243 m_isAttribute = true; | |
244 } | |
245 | |
246 var attr = m_attributes[i]; | |
247 | |
248 | |
249 m_currentAttribute = i; | |
250 m_currentAttributeRead = false; | |
251 m_nodeType = XmlNodeType.Attribute; | |
252 | |
253 m_xmlDepth = m_elementDepth + 1; | |
254 m_qName = attr.QName; | |
255 m_value = attr.Value; | |
256 m_prefix = attr.Prefix; | |
257 } | |
258 | |
259 public override bool MoveToElement() { | |
260 if (m_isAttribute) { | |
261 m_value = null; | |
262 m_nodeType = XmlNodeType.Element; | |
263 m_xmlDepth = m_elementDepth; | |
264 m_prefix = m_elementPrefix; | |
265 m_qName = m_elementQName; | |
266 m_isEmpty = m_elementIsEmpty; | |
267 m_isAttribute = false; | |
268 return true; | |
269 } | |
270 return false; | |
271 } | |
272 | |
273 public override bool MoveToFirstAttribute() { | |
274 if (m_attributes != null && m_attributes.Length > 0) { | |
275 MoveToAttributeImpl(0); | |
276 return true; | |
277 } | |
278 return false; | |
279 } | |
280 | |
281 public override bool MoveToNextAttribute() { | |
282 if (m_isAttribute) { | |
283 var next = m_currentAttribute + 1; | |
284 if (next < AttributeCount) { | |
285 MoveToAttributeImpl(next); | |
286 return true; | |
287 } | |
288 return false; | |
289 } else { | |
290 return MoveToFirstAttribute(); | |
291 } | |
292 | |
293 } | |
294 | |
295 public override bool ReadAttributeValue() { | |
296 if (!m_isAttribute || m_currentAttributeRead) | |
297 return false; | |
298 | |
299 ValueNode(m_attributes[m_currentAttribute].Value); | |
300 m_currentAttributeRead = true; | |
301 return true; | |
302 } | |
303 | |
304 public override void ResolveEntity() { | |
305 /* do nothing */ | |
306 } | |
307 | |
308 /// <summary> | |
309 /// Determines do we need to increase depth after the current node | |
310 /// </summary> | |
311 /// <returns></returns> | |
312 public bool IsSibling() { | |
313 switch (m_nodeType) { | |
314 case XmlNodeType.None: // start document | |
315 case XmlNodeType.Attribute: // after attribute only it's content can be iterated with ReadAttributeValue method | |
316 return false; | |
317 case XmlNodeType.Element: | |
318 // if the elemnt is empty the next element will be it's sibling | |
319 return m_isEmpty; | |
320 | |
321 case XmlNodeType.Document: | |
322 case XmlNodeType.DocumentFragment: | |
323 case XmlNodeType.Entity: | |
324 case XmlNodeType.Text: | |
325 case XmlNodeType.CDATA: | |
326 case XmlNodeType.EntityReference: | |
327 case XmlNodeType.ProcessingInstruction: | |
328 case XmlNodeType.Comment: | |
329 case XmlNodeType.DocumentType: | |
330 case XmlNodeType.Notation: | |
331 case XmlNodeType.Whitespace: | |
332 case XmlNodeType.SignificantWhitespace: | |
333 case XmlNodeType.EndElement: | |
334 case XmlNodeType.EndEntity: | |
335 case XmlNodeType.XmlDeclaration: | |
336 default: | |
337 return true; | |
338 } | |
339 } | |
340 | |
341 void ValueNode(object value) { | |
342 if (!IsSibling()) // the node is nested | |
343 m_xmlDepth++; | |
344 | |
345 m_qName = XmlQualifiedName.Empty; | |
346 m_nodeType = XmlNodeType.Text; | |
347 m_prefix = string.Empty; | |
348 m_value = value; | |
349 m_isEmpty = false; | |
350 m_attributes = null; | |
351 } | |
352 | |
353 void ElementNode(string name, string ns, XmlSimpleAttribute[] attrs, bool empty) { | |
354 if (!IsSibling()) // the node is nested | |
355 m_xmlDepth++; | |
356 | |
357 m_context = new XmlNameContext(m_context); | |
358 List<XmlSimpleAttribute> definedAttrs = null; | |
359 | |
360 // define new namespaces | |
361 if (attrs != null) { | |
362 foreach (var attr in attrs) { | |
363 if (attr.QName.Name == "xmlns") { | |
364 m_context.DefinePrefix(ConvertValueToString(attr.Value), string.Empty); | |
365 } else if (attr.Prefix == m_xmlnsPrefix) { | |
366 m_context.DefinePrefix(ConvertValueToString(attr.Value), attr.QName.Name); | |
367 } else { | |
368 string attrPrefix; | |
369 if (string.IsNullOrEmpty(attr.QName.Namespace)) | |
370 continue; | |
371 | |
372 // auto-define prefixes | |
373 if (!m_context.LookupNamespacePrefix(attr.QName.Namespace, out attrPrefix) || string.IsNullOrEmpty(attrPrefix)) { | |
374 // new namespace prefix added | |
375 attrPrefix = m_context.CreateNamespacePrefix(attr.QName.Namespace); | |
376 attr.Prefix = attrPrefix; | |
377 | |
378 if (definedAttrs == null) | |
379 definedAttrs = new List<XmlSimpleAttribute>(); | |
380 | |
381 definedAttrs.Add(new XmlSimpleAttribute(attrPrefix, m_xmlnsNamespace, m_xmlnsPrefix, attr.QName.Namespace)); | |
382 } | |
383 } | |
384 } | |
385 } | |
386 | |
387 string p; | |
388 // auto-define prefixes | |
389 if (!m_context.LookupNamespacePrefix(ns, out p)) { | |
390 p = m_context.CreateNamespacePrefix(ns); | |
391 if (definedAttrs == null) | |
392 definedAttrs = new List<XmlSimpleAttribute>(); | |
393 | |
394 definedAttrs.Add(new XmlSimpleAttribute(p, m_xmlnsNamespace, m_xmlnsPrefix, ns)); | |
395 } | |
396 | |
397 if (definedAttrs != null) { | |
398 if (attrs != null) | |
399 definedAttrs.AddRange(attrs); | |
400 attrs = definedAttrs.ToArray(); | |
401 } | |
402 | |
403 m_nodeType = XmlNodeType.Element; | |
404 m_qName = new XmlQualifiedName(name, ns); | |
405 m_prefix = p; | |
406 m_value = null; | |
407 m_isEmpty = empty; | |
408 m_attributes = attrs; | |
409 } | |
410 | |
411 void EndElementNode(string name, string ns) { | |
412 if (IsSibling()) // closing the element which has children | |
413 m_xmlDepth--; | |
414 | |
415 string p; | |
416 if (!m_context.LookupNamespacePrefix(ns, out p)) | |
417 throw new Exception($"Failed to lookup namespace '{ns}'"); | |
418 | |
419 m_context = m_context.ParentContext; | |
420 m_nodeType = XmlNodeType.EndElement; | |
421 m_prefix = p; | |
422 m_qName = new XmlQualifiedName(name, ns); | |
423 m_value = null; | |
424 m_attributes = null; | |
425 m_isEmpty = false; | |
426 } | |
427 | |
428 void XmlDeclaration() { | |
429 if (!IsSibling()) // the node is nested | |
430 m_xmlDepth++; | |
431 m_nodeType = XmlNodeType.XmlDeclaration; | |
432 m_qName = new XmlQualifiedName("xml"); | |
433 m_value = "version='1.0'"; | |
434 m_prefix = string.Empty; | |
435 m_attributes = null; | |
436 m_isEmpty = false; | |
437 } | |
438 | |
439 public override bool Read() { | |
440 try { | |
441 string elementName; | |
442 XmlSimpleAttribute[] elementAttrs = null; | |
443 MoveToElement(); | |
444 | |
445 switch (m_position) { | |
446 case JsonXmlReaderPosition.Initial: | |
447 m_jsonLocalName = m_jsonRootName; | |
448 m_jsonSkip = false; | |
449 XmlDeclaration(); | |
450 m_position = JsonXmlReaderPosition.Declaration; | |
451 return true; | |
452 case JsonXmlReaderPosition.Declaration: | |
453 elementAttrs = new[] { | |
454 new XmlSimpleAttribute(m_xsiPrefix, m_xmlnsNamespace, m_xmlnsPrefix, m_xsiNamespace), | |
455 string.IsNullOrEmpty(m_jsonPrefix) ? | |
456 new XmlSimpleAttribute(m_xmlnsPrefix, string.Empty, string.Empty, m_jsonNamespace) : | |
457 new XmlSimpleAttribute(m_jsonPrefix, m_xmlnsNamespace, m_xmlnsPrefix, m_jsonNamespace) | |
458 }; | |
459 break; | |
460 case JsonXmlReaderPosition.ValueElement: | |
461 if (!m_isEmpty) { | |
462 ValueNode(m_parser.ElementValue); | |
463 m_position = JsonXmlReaderPosition.ValueContent; | |
464 return true; | |
465 } else { | |
466 m_position = JsonXmlReaderPosition.ValueEndElement; | |
467 break; | |
468 } | |
469 case JsonXmlReaderPosition.ValueContent: | |
470 EndElementNode(m_jsonValueName, m_jsonNamespace); | |
471 m_position = JsonXmlReaderPosition.ValueEndElement; | |
472 return true; | |
473 case JsonXmlReaderPosition.Eof: | |
474 case JsonXmlReaderPosition.Closed: | |
475 case JsonXmlReaderPosition.Error: | |
476 return false; | |
477 } | |
478 | |
479 while (m_parser.Read()) { | |
480 var jsonName = m_nameTable.Add(m_parser.ElementName); | |
481 | |
482 switch (m_parser.ElementType) { | |
483 case JSONElementType.BeginObject: | |
484 if (!EnterJsonObject(jsonName, out elementName)) | |
485 continue; | |
486 | |
487 m_position = JsonXmlReaderPosition.BeginObject; | |
488 ElementNode(elementName, m_jsonNamespace, elementAttrs, false); | |
489 break; | |
490 case JSONElementType.EndObject: | |
491 if (!LeaveJsonScope(out elementName)) | |
492 continue; | |
493 | |
494 m_position = JsonXmlReaderPosition.EndObject; | |
495 EndElementNode(elementName, m_jsonNamespace); | |
496 break; | |
497 case JSONElementType.BeginArray: | |
498 if (!EnterJsonArray(jsonName, out elementName)) | |
499 continue; | |
500 | |
501 m_position = JsonXmlReaderPosition.BeginArray; | |
502 ElementNode(elementName, m_jsonNamespace, elementAttrs, false); | |
503 break; | |
504 case JSONElementType.EndArray: | |
505 if (!LeaveJsonScope(out elementName)) | |
506 continue; | |
507 | |
508 m_position = JsonXmlReaderPosition.EndArray; | |
509 EndElementNode(elementName, m_jsonNamespace); | |
510 break; | |
511 case JSONElementType.Value: | |
512 if (!VisitJsonValue(jsonName, out m_jsonValueName)) | |
513 continue; | |
514 | |
515 m_position = JsonXmlReaderPosition.ValueElement; | |
516 if (m_parser.ElementValue == null) | |
517 // generate empty element with xsi:nil="true" attribute | |
518 ElementNode( | |
519 m_jsonValueName, | |
520 m_jsonNamespace, | |
521 new[] { | |
522 new XmlSimpleAttribute("nil", m_xsiNamespace, m_xsiPrefix, true) | |
523 }, | |
524 true | |
525 ); | |
526 else | |
527 ElementNode(m_jsonValueName, m_jsonNamespace, elementAttrs, m_parser.ElementValue as string == string.Empty); | |
528 break; | |
529 default: | |
530 throw new Exception($"Unexpected JSON element {m_parser.ElementType}: {m_parser.ElementName}"); | |
531 } | |
532 return true; | |
533 } | |
534 | |
535 m_position = JsonXmlReaderPosition.Eof; | |
536 return false; | |
537 } catch { | |
538 m_position = JsonXmlReaderPosition.Error; | |
539 throw; | |
540 } | |
541 } | |
542 | |
543 void SaveJsonName() { | |
544 m_jsonNameStack.Push(new JsonContext { | |
545 skip = m_jsonSkip, | |
546 localName = m_jsonLocalName | |
547 }); | |
548 | |
549 } | |
550 | |
551 bool EnterJsonObject(string name, out string elementName) { | |
552 SaveJsonName(); | |
553 m_jsonSkip = false; | |
554 | |
555 if (string.IsNullOrEmpty(name)) { | |
556 if (m_jsonNameStack.Count != 1 && !m_jsonFlattenArrays) | |
557 m_jsonLocalName = m_jsonArrayItemName; | |
558 } else { | |
559 m_jsonLocalName = name; | |
560 } | |
561 | |
562 elementName = m_jsonLocalName; | |
563 return true; | |
564 } | |
565 | |
566 /// <summary> | |
567 /// Called when JSON parser visits BeginArray ('[') element. | |
568 /// </summary> | |
569 /// <param name="name">Optional property name if the array is the member of an object</param> | |
570 /// <returns>true if element should be emited, false otherwise</returns> | |
571 bool EnterJsonArray(string name, out string elementName) { | |
572 SaveJsonName(); | |
573 | |
574 if (string.IsNullOrEmpty(name)) { | |
575 // m_jsonNameStack.Count == 1 means the root node | |
576 if (m_jsonNameStack.Count != 1 && !m_jsonFlattenArrays) | |
577 m_jsonLocalName = m_jsonArrayItemName; | |
578 | |
579 m_jsonSkip = false; // we should not flatten arrays inside arrays or in the document root | |
580 } else { | |
581 m_jsonLocalName = name; | |
582 m_jsonSkip = m_jsonFlattenArrays; | |
583 } | |
584 elementName = m_jsonLocalName; | |
585 | |
586 return !m_jsonSkip; | |
587 } | |
588 | |
589 bool VisitJsonValue(string name, out string elementName) { | |
590 if (string.IsNullOrEmpty(name)) { | |
591 // m_jsonNameStack.Count == 0 means that JSON document consists from simple value | |
592 elementName = (m_jsonNameStack.Count == 0 || m_jsonFlattenArrays) ? m_jsonLocalName : m_jsonArrayItemName; | |
593 } else { | |
594 elementName = name; | |
595 } | |
596 return true; | |
597 } | |
598 | |
599 bool LeaveJsonScope(out string elementName) { | |
600 elementName = m_jsonLocalName; | |
601 var skip = m_jsonSkip; | |
602 | |
603 var prev = m_jsonNameStack.Pop(); | |
604 m_jsonLocalName = prev.localName; | |
605 m_jsonSkip = prev.skip; | |
606 | |
607 return !skip; | |
608 } | |
609 | |
610 public override string ToString() { | |
611 switch (NodeType) { | |
612 case XmlNodeType.Element: | |
613 return $"<{Name} {string.Join(" ", (m_attributes ?? new XmlSimpleAttribute[0]).Select(x => $"{x.Prefix}{(string.IsNullOrEmpty(x.Prefix) ? "" : ":")}{x.QName.Name}='{ConvertValueToString(x.Value)}'"))} {(IsEmptyElement ? "/" : "")}>"; | |
614 case XmlNodeType.Attribute: | |
615 return $"@{Name}"; | |
616 case XmlNodeType.Text: | |
617 return $"{Value}"; | |
618 case XmlNodeType.CDATA: | |
619 return $"<![CDATA[{Value}]]>"; | |
620 case XmlNodeType.EntityReference: | |
621 return $"&{Name};"; | |
622 case XmlNodeType.EndElement: | |
623 return $"</{Name}>"; | |
624 default: | |
625 return $".{NodeType} {Name} {Value}"; | |
626 } | |
627 } | |
628 } | |
629 } |