0
|
1 using System;
|
|
2 using System.Globalization;
|
|
3 using System.Text;
|
|
4 using System.Xml;
|
|
5
|
|
6 using NUnit.Framework;
|
|
7
|
|
8 using BLToolkit.Mapping;
|
|
9 using BLToolkit.Reflection;
|
|
10
|
|
11 namespace HowTo.Mapping
|
|
12 {
|
|
13 public class JsonMapper : MapDataDestinationBase, IMapDataDestinationList, ISupportMapping
|
|
14 {
|
|
15 private static readonly long InitialJavaScriptDateTicks = new DateTime(1970, 1, 1).Ticks;
|
|
16
|
|
17 private string[] _fieldNames;
|
|
18 private readonly StringBuilder _sb;
|
|
19 private MappingSchema _mappingSchema;
|
|
20 private bool _scalar;
|
|
21 private bool _first;
|
|
22 private bool _firstElement;
|
|
23 private int _indent;
|
|
24
|
|
25 public JsonMapper() : this(new StringBuilder(), 0)
|
|
26 {
|
|
27 }
|
|
28
|
|
29 public JsonMapper(StringBuilder sb) : this(sb, 0)
|
|
30 {
|
|
31 }
|
|
32
|
|
33 public JsonMapper(StringBuilder sb, int indent)
|
|
34 {
|
|
35 _sb = sb;
|
|
36 _indent = indent;
|
|
37 }
|
|
38
|
|
39 public override Type GetFieldType(int index)
|
|
40 {
|
|
41 // Same as typeof(object)
|
|
42 //
|
|
43 return null;
|
|
44 }
|
|
45
|
|
46 public override int GetOrdinal(string name)
|
|
47 {
|
|
48 return Array.IndexOf(_fieldNames, name);
|
|
49 }
|
|
50
|
|
51 public override void SetValue(object o, int index, object value)
|
|
52 {
|
|
53 SetValue(o, _fieldNames[index], value);
|
|
54 }
|
|
55
|
|
56 public override void SetValue(object o, string name, object value)
|
|
57 {
|
|
58 if (!_scalar)
|
|
59 {
|
|
60 // Do not Json null values until it's an array
|
|
61 //
|
|
62 if (value == null || (value is XmlNode && IsEmptyNode((XmlNode)value)))
|
|
63 return;
|
|
64
|
|
65 if (_first)
|
|
66 _first = false;
|
|
67 else
|
|
68 _sb
|
|
69 .Append(',')
|
|
70 .AppendLine()
|
|
71 ;
|
|
72
|
|
73 for (int i = 0; i < _indent; ++i)
|
|
74 _sb.Append(' ');
|
|
75
|
|
76 _sb
|
|
77 .Append('"')
|
|
78 .Append(name)
|
|
79 .Append("\":")
|
|
80 ;
|
|
81 }
|
|
82
|
|
83 if (value == null)
|
|
84 _sb.Append("null");
|
|
85 else
|
|
86 {
|
|
87 switch (Type.GetTypeCode(value.GetType()))
|
|
88 {
|
|
89 case TypeCode.Empty:
|
|
90 case TypeCode.DBNull:
|
|
91 _sb.Append("null");
|
|
92 break;
|
|
93 case TypeCode.Boolean:
|
|
94 _sb.Append((bool)value? "true": "false");
|
|
95 break;
|
|
96 case TypeCode.Char:
|
|
97 _sb
|
|
98 .Append('\'')
|
|
99 .Append((char)value)
|
|
100 .Append('\'')
|
|
101 ;
|
|
102 break;
|
|
103 case TypeCode.SByte:
|
|
104 case TypeCode.Int16:
|
|
105 case TypeCode.Int32:
|
|
106 case TypeCode.Int64:
|
|
107 case TypeCode.Byte:
|
|
108 case TypeCode.UInt16:
|
|
109 case TypeCode.UInt32:
|
|
110 case TypeCode.UInt64:
|
|
111 case TypeCode.Single:
|
|
112 case TypeCode.Double:
|
|
113 case TypeCode.Decimal:
|
|
114 _sb.Append(((IFormattable)value).ToString(null, CultureInfo.InvariantCulture));
|
|
115 break;
|
|
116 case TypeCode.DateTime:
|
|
117 _sb
|
|
118 .Append("new Date(")
|
|
119 .Append((((DateTime)value).Ticks - InitialJavaScriptDateTicks)/10000)
|
|
120 .Append(")");
|
|
121 break;
|
|
122 case TypeCode.String:
|
|
123 _sb
|
|
124 .Append('"')
|
|
125 .Append(encode((string)value))
|
|
126 .Append('"')
|
|
127 ;
|
|
128 break;
|
|
129 default:
|
|
130 if (value is XmlNode)
|
|
131 {
|
|
132 if (IsEmptyNode((XmlNode) value))
|
|
133 _sb.Append("null");
|
|
134 else
|
|
135 WriteXmlJson((XmlNode)value);
|
|
136 }
|
|
137 else
|
|
138 {
|
|
139 JsonMapper inner = new JsonMapper(_sb, _indent + 1);
|
|
140
|
|
141 if (value.GetType().IsArray)
|
|
142 _mappingSchema.MapSourceListToDestinationList(
|
|
143 _mappingSchema.GetDataSourceList(value), inner);
|
|
144 else
|
|
145 _mappingSchema.MapSourceToDestination(
|
|
146 _mappingSchema.GetDataSource(value), value, inner, inner);
|
|
147 }
|
|
148 break;
|
|
149 }
|
|
150 }
|
|
151 }
|
|
152
|
|
153 private static string encode(string value)
|
|
154 {
|
|
155 return value.Replace("\r\n", "\\r")
|
|
156 .Replace("\n\r", "\\r")
|
|
157 .Replace("\n", "\\r")
|
|
158 .Replace("\r", "\\r")
|
|
159 .Replace("\"","\\\"");
|
|
160 }
|
|
161
|
|
162 private void WriteXmlJson(XmlNode node)
|
|
163 {
|
|
164 XmlNode textNode = GetTextNode(node);
|
|
165 if (textNode != null)
|
|
166 {
|
|
167 _sb
|
|
168 .Append("\"")
|
|
169 .Append(encode(textNode.Value))
|
|
170 .Append('\"')
|
|
171 ;
|
|
172 }
|
|
173 else
|
|
174 {
|
|
175
|
|
176 bool first = true;
|
|
177
|
|
178 _sb.Append('{');
|
|
179
|
|
180 if (node.Attributes != null)
|
|
181 {
|
|
182 foreach (XmlAttribute attr in node.Attributes)
|
|
183 {
|
|
184 if (first)
|
|
185 first = false;
|
|
186 else
|
|
187 _sb.Append(',');
|
|
188
|
|
189 _sb
|
|
190 .Append("\"@")
|
|
191 .Append(attr.Name)
|
|
192 .Append("\":\"")
|
|
193 .Append(encode(attr.Value))
|
|
194 .Append('\"')
|
|
195 ;
|
|
196 }
|
|
197 }
|
|
198
|
|
199 foreach (XmlNode child in node.ChildNodes)
|
|
200 {
|
|
201 if (IsWhitespace(child) || IsEmptyNode(child))
|
|
202 continue;
|
|
203
|
|
204 if (first)
|
|
205 first = false;
|
|
206 else
|
|
207 _sb.Append(',');
|
|
208
|
|
209 if (child is XmlText)
|
|
210 _sb
|
|
211 .Append("\"#text\":\"")
|
|
212 .Append(encode(child.Value))
|
|
213 .Append('\"')
|
|
214 ;
|
|
215 else if (child is XmlElement)
|
|
216 {
|
|
217 _sb
|
|
218 .Append('"')
|
|
219 .Append(child.Name)
|
|
220 .Append("\":")
|
|
221 ;
|
|
222 WriteXmlJson(child);
|
|
223 }
|
|
224 else
|
|
225 System.Diagnostics.Debug.Fail("Unexpected node type " + child.GetType().FullName);
|
|
226 }
|
|
227 _sb.Append('}');
|
|
228 }
|
|
229 }
|
|
230
|
|
231 private static bool IsWhitespace(XmlNode node)
|
|
232 {
|
|
233 switch (node.NodeType)
|
|
234 {
|
|
235 case XmlNodeType.Comment:
|
|
236 case XmlNodeType.Whitespace:
|
|
237 case XmlNodeType.SignificantWhitespace:
|
|
238 return true;
|
|
239 }
|
|
240 return false;
|
|
241 }
|
|
242
|
|
243 private static bool IsEmptyNode(XmlNode node)
|
|
244 {
|
|
245 if (node.Attributes != null && node.Attributes.Count > 0)
|
|
246 return false;
|
|
247
|
|
248 if (node.HasChildNodes)
|
|
249 foreach (XmlNode childNode in node.ChildNodes)
|
|
250 {
|
|
251 if (IsWhitespace(childNode) || IsEmptyNode(childNode))
|
|
252 continue;
|
|
253
|
|
254 // Not a whitespace, nor inner empty node.
|
|
255 //
|
|
256 return false;
|
|
257 }
|
|
258
|
|
259 return node.Value == null;
|
|
260 }
|
|
261
|
|
262 private static XmlNode GetTextNode(XmlNode node)
|
|
263 {
|
|
264 if (node.Attributes != null && node.Attributes.Count > 0)
|
|
265 return null;
|
|
266
|
|
267 XmlNode textNode = null;
|
|
268
|
|
269 foreach (XmlNode childNode in node.ChildNodes)
|
|
270 {
|
|
271 // Ignore all whitespace.
|
|
272 //
|
|
273 if (IsWhitespace(childNode))
|
|
274 continue;
|
|
275
|
|
276 if (childNode is XmlText)
|
|
277 {
|
|
278 // More then one text node.
|
|
279 //
|
|
280 if (textNode != null)
|
|
281 return null;
|
|
282
|
|
283 // First text node.
|
|
284 //
|
|
285 textNode = childNode;
|
|
286 }
|
|
287 else
|
|
288 // Not a text node - break;
|
|
289 //
|
|
290 return null;
|
|
291 }
|
|
292
|
|
293 return textNode;
|
|
294 }
|
|
295
|
|
296 #region ISupportMapping Members
|
|
297
|
|
298 void ISupportMapping.BeginMapping(InitContext initContext)
|
|
299 {
|
|
300 _first = true;
|
|
301 _mappingSchema = initContext.MappingSchema;
|
|
302 _fieldNames = new string[initContext.DataSource.Count];
|
|
303
|
|
304 for (int i = 0; i < _fieldNames.Length; ++i)
|
|
305 _fieldNames[i] = initContext.DataSource.GetName(i);
|
|
306
|
|
307 _scalar = _fieldNames.Length == 1 && string.IsNullOrEmpty(_fieldNames[0]);
|
|
308
|
|
309 if (_scalar)
|
|
310 return;
|
|
311
|
|
312 if (_fieldNames.Length <= 1)
|
|
313 {
|
|
314 // Reset the indent since output is a single line.
|
|
315 //
|
|
316 _indent = 0;
|
|
317 _sb.Append('{');
|
|
318 }
|
|
319 else
|
|
320 {
|
|
321 if (_indent > 0)
|
|
322 _sb.AppendLine();
|
|
323
|
|
324 for (int i = 0; i < _indent; ++i)
|
|
325 _sb.Append(' ');
|
|
326
|
|
327 _sb
|
|
328 .Append('{')
|
|
329 .AppendLine()
|
|
330 ;
|
|
331 }
|
|
332 }
|
|
333
|
|
334 void ISupportMapping.EndMapping(InitContext initContext)
|
|
335 {
|
|
336 if (_scalar)
|
|
337 return;
|
|
338
|
|
339 if (_fieldNames.Length > 1)
|
|
340 _sb.AppendLine();
|
|
341
|
|
342 for (int i = 0; i < _indent; ++i)
|
|
343 _sb.Append(' ');
|
|
344 _sb.Append('}');
|
|
345 }
|
|
346
|
|
347 #endregion
|
|
348
|
|
349 #region IMapDataDestinationList Members
|
|
350
|
|
351 void IMapDataDestinationList.InitMapping(InitContext initContext)
|
|
352 {
|
|
353 _firstElement = true;
|
|
354 _sb.Append('[');
|
|
355 }
|
|
356
|
|
357 IMapDataDestination IMapDataDestinationList.GetDataDestination(InitContext initContext)
|
|
358 {
|
|
359 return this;
|
|
360 }
|
|
361
|
|
362 object IMapDataDestinationList.GetNextObject(InitContext initContext)
|
|
363 {
|
|
364 if (_firstElement)
|
|
365 _firstElement = false;
|
|
366 else
|
|
367 _sb.Append(',');
|
|
368
|
|
369 return this;
|
|
370 }
|
|
371
|
|
372 void IMapDataDestinationList.EndMapping(InitContext initContext)
|
|
373 {
|
|
374 _sb.Append(']');
|
|
375 }
|
|
376
|
|
377 #endregion
|
|
378
|
|
379 public override string ToString()
|
|
380 {
|
|
381 return _sb.ToString();
|
|
382 }
|
|
383 }
|
|
384
|
|
385 [TestFixture]
|
|
386 public class MapToJson
|
|
387 {
|
|
388 public class Inner
|
|
389 {
|
|
390 public string Name = "inner \"object \n name";
|
|
391 }
|
|
392
|
|
393 public class Inner2
|
|
394 {
|
|
395 public string Name;
|
|
396 public int Value;
|
|
397 }
|
|
398
|
|
399 public class SourceObject
|
|
400 {
|
|
401 public string Foo = "Foo";
|
|
402 public double Bar = 1.23;
|
|
403 public DateTime Baz = DateTime.Today;
|
|
404 [MapIgnore(false)]
|
|
405 public Inner Inner = new Inner();
|
|
406 [MapIgnore(false)]
|
|
407 public Inner2 Inner2 = new Inner2();
|
|
408 public string[] StrArray = {"One", "Two", "Three"};
|
|
409 }
|
|
410
|
|
411 [Test]
|
|
412 public void Test()
|
|
413 {
|
|
414 JsonMapper jm = new JsonMapper(new StringBuilder(256));
|
|
415
|
|
416 Map./*[a]*/MapSourceToDestination/*[/a]*/(Map.GetObjectMapper(typeof(SourceObject)), new SourceObject(), jm, jm);
|
|
417 Console.Write(jm.ToString());
|
|
418
|
|
419 // Expected output:
|
|
420 //
|
|
421 // {
|
|
422 // "Foo":"Foo",
|
|
423 // "Bar":1.23,
|
|
424 // "Baz":new Date(11823840000000000),
|
|
425 // "Inner":{ "Name":"inner \"object \r name"},
|
|
426 // "Inner2":
|
|
427 // {
|
|
428 // "Name":null,
|
|
429 // "Value":0
|
|
430 // },
|
|
431 // "StrArray":["One","Two","Three"]
|
|
432 // }
|
|
433 }
|
|
434 }
|
|
435 }
|