Merge pull request #82 from Unity-Technologies/master-gc-race
[mono.git] / mcs / class / System.ServiceModel.Web / System.Runtime.Serialization.Json / JsonWriter.cs
1 //
2 // JsonWriter.cs
3 //
4 // Author:
5 //      Atsushi Enomoto  <atsushi@ximian.com>
6 //
7 // Copyright (C) 2007 Novell, Inc (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28 using System;
29 using System.Collections.Generic;
30 using System.Globalization;
31 using System.IO;
32 using System.Text;
33 using System.Xml;
34
35 namespace System.Runtime.Serialization.Json
36 {
37         class JsonWriter : XmlDictionaryWriter, IXmlJsonWriterInitializer
38         {
39                 enum ElementType
40                 {
41                         None,
42                         Null,
43                         Object,
44                         Array,
45                         String,
46                         Number,
47                         Boolean,
48                 }
49
50                 Stream output;
51                 bool close_output;
52                 WriteState state;
53                 Stack<ElementType> element_kinds = new Stack<ElementType> ();
54                 Stack<bool> first_content_flags = new Stack<bool> ();
55                 string attr_name, attr_value, runtime_type;
56                 Encoding encoding;
57                 byte [] encbuf = new byte [1024];
58                 bool no_string_yet = true, is_null, is_ascii_single;
59
60                 public JsonWriter (Stream stream, Encoding encoding, bool closeOutput)
61                 {
62                         SetOutput (stream, encoding, closeOutput);
63                 }
64
65                 public void SetOutput (Stream stream, Encoding encoding, bool ownsStream)
66                 {
67                         if (stream == null)
68                                 throw new ArgumentNullException ("stream");
69                         if (encoding == null)
70                                 throw new ArgumentNullException ("encoding");
71                         output = stream;
72                         this.encoding = encoding;
73                         close_output = ownsStream;
74 #if MOONLIGHT
75                         is_ascii_single = encoding is UTF8Encoding;
76 #else
77                         is_ascii_single = encoding is UTF8Encoding || encoding.IsSingleByte;
78 #endif
79                 }
80
81                 void CheckState ()
82                 {
83                         switch (state) {
84                         case WriteState.Closed:
85                         case WriteState.Error:
86                                 throw new InvalidOperationException (String.Format ("This XmlDictionaryReader is already at '{0}' state", state));
87                         }
88                 }
89
90                 // copied from System.Silverlight JavaScriptSerializer.
91                 static string EscapeStringLiteral (string input)
92                 {
93                         StringBuilder sb = null;
94                         int i = 0, start = 0;
95                         for (; i < input.Length; i++) {
96                                 switch (input [i]) {
97                                 case '"':
98                                         AppendBuffer (ref sb, input, start, i, @"\""");
99                                         break;
100                                 case '\\':
101                                         AppendBuffer (ref sb, input, start, i, @"\\");
102                                         break;
103                                 case '/':
104                                         AppendBuffer (ref sb, input, start, i, @"\/");
105                                         break;
106                                 case '\x8':
107                                         AppendBuffer (ref sb, input, start, i, @"\b");
108                                         break;
109                                 case '\f':
110                                         AppendBuffer (ref sb, input, start, i, @"\f");
111                                         break;
112                                 case '\n':
113                                         AppendBuffer (ref sb, input, start, i, /*@"\n"*/@"\u000a");
114                                         break;
115                                 case '\r':
116                                         AppendBuffer (ref sb, input, start, i, /*@"\r"*/@"\u000d");
117                                         break;
118                                 case '\t':
119                                         AppendBuffer (ref sb, input, start, i, /*@"\t"*/@"\u0009");
120                                         break;
121                                 default:
122                                         continue;
123                                 }
124                                 start = i + 1;
125                         }
126                         string remaining = input.Substring (start, i - start);
127                         if (sb != null)
128                                 return sb.Append (remaining).ToString ();
129                         else
130                                 return remaining;
131                 }
132
133                 static void AppendBuffer (ref StringBuilder sb, string input, int start, int i, string append)
134                 {
135                         if (sb == null)
136                                 sb = new StringBuilder ();
137                         if (i != start)
138                                 sb.Append (input, start, i - start);
139                         sb.Append (append);
140                 }
141
142                 public override WriteState WriteState {
143                         get { return state; }
144                 }
145
146                 public override void Close ()
147                 {
148                         // close all open elements
149                         while (element_kinds.Count > 0)
150                                 WriteEndElement ();
151
152                         if (close_output)
153                                 output.Close ();
154                         else
155                                 output.Flush ();
156
157                         state = WriteState.Closed;
158                 }
159
160                 public override void Flush ()
161                 {
162                         output.Flush ();
163                 }
164
165                 public override void WriteStartElement (string prefix, string localName, string ns)
166                 {
167                         CheckState ();
168
169                         if (localName == null)
170                                 throw new ArgumentNullException ("localName");
171                         else if (localName.Length == 0)
172                                 throw new ArgumentException ("Empty string is not a valid localName in this XmlDictionaryWriter");
173
174                         if (!String.IsNullOrEmpty (ns))
175                                 throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter");
176                         if (!String.IsNullOrEmpty (prefix))
177                                 throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter");
178
179                         if (state == WriteState.Attribute)
180                                 WriteEndAttribute ();
181                         if (state == WriteState.Element)
182                                 CloseStartElement ();
183
184                         else if (state != WriteState.Start && element_kinds.Count == 0)
185                                 throw new XmlException ("This XmlDictionaryWriter does not support multiple top-level elements");
186
187                         if (element_kinds.Count == 0) {
188                                 if (localName != "root")
189                                         throw new XmlException ("Only 'root' is allowed for the name of the top-level element");
190                         } else {
191                                 switch (element_kinds.Peek ()) {
192                                 case ElementType.Array:
193                                         if (localName != "item")
194                                                 throw new XmlException ("Only 'item' is allowed as a content element of an array");
195                                         break;
196                                 case ElementType.String:
197                                         throw new XmlException ("Mixed content is not allowed in this XmlDictionaryWriter");
198                                 case ElementType.Null:
199                                         throw new XmlException ("Current type is null and writing element inside null is not allowed");
200                                 case ElementType.None:
201                                         throw new XmlException ("Before writing a child element, an element needs 'type' attribute to indicate whether the element is a JSON array or a JSON object in this XmlDictionaryWriter");
202                                 }
203
204                                 if (first_content_flags.Peek ()) {
205                                         first_content_flags.Pop ();
206                                         first_content_flags.Push (false);
207                                 }
208                                 else
209                                         OutputAsciiChar (',');
210
211                                 if (element_kinds.Peek () != ElementType.Array) {
212                                         OutputAsciiChar ('"');
213                                         OutputString (localName);
214                                         OutputAsciiChar ('\"');
215                                         OutputAsciiChar (':');
216                                 }
217                         }
218
219                         element_kinds.Push (ElementType.None); // undetermined yet
220
221                         state = WriteState.Element;
222                 }
223
224                 public override void WriteEndElement ()
225                 {
226                         CheckState ();
227
228                         if (state == WriteState.Attribute)
229                                 throw new XmlException ("Cannot end element when an attribute is being written");
230                         if (state == WriteState.Element)
231                                 CloseStartElement ();
232
233                         if (element_kinds.Count == 0)
234                                 throw new XmlException ("There is no open element to close");
235                         switch (element_kinds.Pop ()) {
236                         case ElementType.String:
237                                 if (!is_null) {
238                                         if (no_string_yet)
239                                                 OutputAsciiChar ('"');
240                                         OutputAsciiChar ('"');
241                                 }
242                                 no_string_yet = true;
243                                 is_null = false;
244                                 break;
245                         case ElementType.Array:
246                                 OutputAsciiChar (']');
247                                 break;
248                         case ElementType.Object:
249                                 OutputAsciiChar ('}');
250                                 break;
251                         }
252
253                         // not sure if it is correct though ...
254                         state = WriteState.Content;
255                         first_content_flags.Pop ();
256                 }
257
258                 public override void WriteFullEndElement ()
259                 {
260                         WriteEndElement (); // no such difference in JSON.
261                 }
262
263                 public override void WriteStartAttribute (string prefix, string localName, string ns)
264                 {
265                         CheckState ();
266
267                         if (state != WriteState.Element)
268                                 throw new XmlException ("Cannot write attribute as this XmlDictionaryWriter is not at element state");
269
270                         if (!String.IsNullOrEmpty (ns))
271                                 throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter");
272                         if (!String.IsNullOrEmpty (prefix))
273                                 throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter");
274
275                         if (localName != "type" && localName != "__type")
276                                 throw new ArgumentException ("Only 'type' and '__type' are allowed as an attribute name in this XmlDictionaryWriter");
277
278                         if (state != WriteState.Element)
279                                 throw new InvalidOperationException (String.Format ("Attribute cannot be written in {0} mode", state));
280
281                         attr_name = localName;
282                         state = WriteState.Attribute;
283                 }
284
285                 public override void WriteEndAttribute ()
286                 {
287                         CheckState ();
288
289                         if (state != WriteState.Attribute)
290                                 throw new XmlException ("Cannot close attribute, as this XmlDictionaryWriter is not at attribute state");
291
292                         if (attr_name == "type") {
293                                 switch (attr_value) {
294                                 case "object":
295                                         element_kinds.Pop ();
296                                         element_kinds.Push (ElementType.Object);
297                                         OutputAsciiChar ('{');
298                                         break;
299                                 case "array":
300                                         element_kinds.Pop ();
301                                         element_kinds.Push (ElementType.Array);
302                                         OutputAsciiChar ('[');
303                                         break;
304                                 case "number":
305                                         element_kinds.Pop ();
306                                         element_kinds.Push (ElementType.Number);
307                                         break;
308                                 case "boolean":
309                                         element_kinds.Pop ();
310                                         element_kinds.Push (ElementType.Boolean);
311                                         break;
312                                 case "string":
313                                         element_kinds.Pop ();
314                                         element_kinds.Push (ElementType.String);
315                                         break;
316                                 case "null":
317                                         element_kinds.Pop ();
318                                         element_kinds.Push (ElementType.Null);
319                                         OutputString ("null");
320                                         break;
321                                 default:
322                                         throw new XmlException (String.Format ("Unexpected type attribute value '{0}'", attr_value));
323                                 }
324                         }
325                         else
326                                 runtime_type = attr_value;
327
328                         state = WriteState.Element;
329                         attr_value = null;
330                 }
331
332                 void CloseStartElement ()
333                 {
334                         if (element_kinds.Peek () == ElementType.None) {
335                                 element_kinds.Pop ();
336                                 element_kinds.Push (ElementType.String);
337                                 no_string_yet = true;
338                                 is_null = false;
339                         }
340
341                         first_content_flags.Push (true);
342
343                         if (runtime_type != null) {
344                                 OutputString ("\"__type\":\"");
345                                 OutputString (runtime_type);
346                                 OutputAsciiChar ('\"');
347                                 runtime_type = null;
348                                 first_content_flags.Pop ();
349                                 first_content_flags.Push (false);
350                         }
351                 }
352
353                 public override void WriteString (string text)
354                 {
355                         CheckState ();
356
357                         if (state == WriteState.Start)
358                                 throw new InvalidOperationException ("Top-level content string is not allowed in this XmlDictionaryWriter");
359
360                         if (state == WriteState.Element) {
361                                 CloseStartElement ();
362                                 state = WriteState.Content;
363                         }
364
365                         if (state == WriteState.Attribute)
366                                 attr_value += text;
367                         else if (text == null) {
368                                 no_string_yet = false;
369                                 is_null = true;
370                                 if (element_kinds.Peek () != ElementType.Null)
371                                         OutputString ("null");
372                         } else {
373                                 switch (element_kinds.Peek ()) {
374                                 case ElementType.String:
375                                         if (no_string_yet) {
376                                                 OutputAsciiChar ('"');
377                                                 no_string_yet = false;
378                                         }
379                                         break;
380                                 case ElementType.Number:
381                                         // .NET is buggy here, it just outputs raw string, which results in invalid JSON format.
382                                         bool isString = false;
383                                         switch (text) {
384                                         case "INF":
385                                         case "-INF":
386                                         case "NaN":
387                                                 isString = true;
388                                                 break;
389                                         }
390                                         if (isString) {
391                                                 element_kinds.Pop ();
392                                                 element_kinds.Push (ElementType.String);
393                                                 goto case ElementType.String;
394                                         }
395                                         break;
396                                 case ElementType.Boolean:
397                                         break;
398                                 default:
399                                         throw new XmlException (String.Format ("Simple content string is allowed only for string, number and boolean types and not for {0} type", element_kinds.Peek ()));
400                                 }
401
402                                 OutputString (EscapeStringLiteral (text));
403                         }
404                 }
405
406                 #region mostly-ignored operations
407
408                 public override string LookupPrefix (string ns)
409                 {
410                         // Since there is no way to declare namespaces in
411                         // this writer, it always returns fixed results.
412                         if (ns == null)
413                                 throw new ArgumentNullException ("ns");
414                         else if (ns.Length == 0)
415                                 return String.Empty;
416                         else if (ns == "http://www.w3.org/2000/xmlns/")
417                                 return "xmlns";
418                         else if (ns == "http://www.w3.org/XML/1998/namespace")
419                                 return "xml";
420                         return null;
421                 }
422
423                 public override void WriteStartDocument ()
424                 {
425                         CheckState ();
426                 }
427
428                 public override void WriteStartDocument (bool standalone)
429                 {
430                         CheckState ();
431                 }
432
433                 public override void WriteEndDocument ()
434                 {
435                         CheckState ();
436                 }
437
438                 #endregion
439
440                 #region unsupported operations
441
442                 public override void WriteDocType (string name, string pubid, string sysid, string intSubset)
443                 {
444                         CheckState ();
445
446                         throw new NotSupportedException ("This XmlDictionaryWriter does not support writing doctype declaration");
447                 }
448
449                 public override void WriteComment (string text)
450                 {
451                         CheckState ();
452
453                         throw new NotSupportedException ("This XmlDictionaryWriter does not support writing comment");
454                 }
455
456                 public override void WriteEntityRef (string text)
457                 {
458                         CheckState ();
459
460                         throw new NotSupportedException ("This XmlDictionaryWriter does not support writing entity reference");
461                 }
462
463                 public override void WriteProcessingInstruction (string target, string data)
464                 {
465                         CheckState ();
466
467                         if (String.Compare (target, "xml", StringComparison.OrdinalIgnoreCase) != 0)
468                                 throw new ArgumentException ("This XmlDictionaryWriter does not support writing processing instruction");
469                 }
470
471                 #endregion
472
473                 #region WriteString() variants
474
475                 public override void WriteRaw (string text)
476                 {
477 #if MOONLIGHT
478                         OutputString (text);
479 #else
480                         WriteString (text);
481 #endif
482                 }
483
484                 public override void WriteRaw (char [] chars, int start, int length)
485                 {
486                         WriteChars (chars, start, length);
487                 }
488
489                 public override void WriteCData (string text)
490                 {
491                         WriteString (text);
492                 }
493
494                 public override void WriteCharEntity (char entity)
495                 {
496                         WriteString (entity.ToString ());
497                 }
498
499                 public override void WriteChars (char [] chars, int start, int length)
500                 {
501                         WriteString (new string (chars, start, length));
502                 }
503
504                 public override void WriteSurrogateCharEntity (char high, char low)
505                 {
506                         WriteChars (new char [] {high, low}, 0, 2);
507                 }
508
509                 public override void WriteBase64 (byte [] bytes, int start, int length)
510                 {
511                         WriteString (Convert.ToBase64String (bytes, start, length));
512                 }
513
514                 public override void WriteWhitespace (string text)
515                 {
516                         if (text == null)
517                                 throw new ArgumentNullException ("text");
518                         for (int i = 0; i < text.Length; i++) {
519                                 if (text [i] != ' ') {
520                                         for (int j = i; j < text.Length; j++) {
521                                                 switch (text [j]) {
522                                                 case '\t':
523                                                 case ' ':
524                                                 case '\n':
525                                                 case '\r':
526                                                         continue;
527                                                 default:
528                                                         throw new ArgumentException (String.Format ("WriteWhitespace() does not accept non-whitespace character '{0}'", text [j]));
529                                                 }
530                                         }
531                                         break;
532                                 }
533                         }
534                         WriteString (text);
535                 }
536
537                 char [] char_buf = new char [1];
538                 void OutputAsciiChar (char c)
539                 {
540                         if (is_ascii_single)
541                                 output.WriteByte ((byte) c);
542                         else {
543                                 char_buf [0] = c;
544                                 int size = encoding.GetBytes (char_buf, 0, 1, encbuf, 0);
545                                 output.Write (encbuf, 0, size);
546                         }
547                 }
548
549                 void OutputString (string s)
550                 {
551                         int size = encoding.GetByteCount (s);
552                         if (encbuf.Length < size)
553                                 encbuf = new byte [size];
554                         size = encoding.GetBytes (s, 0, s.Length, encbuf, 0);
555                         output.Write (encbuf, 0, size);
556                 }
557
558                 #endregion
559         }
560 }