2010-01-20 Zoltan Varga <vargaz@gmail.com>
[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                         Object,
43                         Array,
44                         String,
45                         Number,
46                         Boolean,
47                 }
48
49                 Stream output;
50                 bool close_output;
51                 WriteState state;
52                 Stack<ElementType> element_kinds = new Stack<ElementType> ();
53                 Stack<bool> first_content_flags = new Stack<bool> ();
54                 string attr_name, attr_value, runtime_type;
55                 Encoding encoding;
56                 byte [] encbuf = new byte [1024];
57                 bool no_string_yet = true, is_null;
58
59                 public JsonWriter (Stream stream, Encoding encoding, bool closeOutput)
60                 {
61                         SetOutput (stream, encoding, closeOutput);
62                 }
63
64                 public void SetOutput (Stream stream, Encoding encoding, bool ownsStream)
65                 {
66                         if (stream == null)
67                                 throw new ArgumentNullException ("stream");
68                         if (encoding == null)
69                                 throw new ArgumentNullException ("encoding");
70                         output = stream;
71                         this.encoding = encoding;
72                         close_output = ownsStream;
73                 }
74
75                 void CheckState ()
76                 {
77                         switch (state) {
78                         case WriteState.Closed:
79                         case WriteState.Error:
80                                 throw new InvalidOperationException (String.Format ("This XmlDictionaryReader is already at '{0}' state", state));
81                         }
82                 }
83
84                 // copied from System.Silverlight JavaScriptSerializer.
85                 static string EscapeStringLiteral (string input)
86                 {
87                         StringBuilder sb = null;
88                         int i = 0, start = 0;
89                         for (; i < input.Length; i++) {
90                                 switch (input [i]) {
91                                 case '"':
92                                         AppendBuffer (ref sb, input, start, i, @"\""");
93                                         break;
94                                 case '\\':
95                                         AppendBuffer (ref sb, input, start, i, @"\\");
96                                         break;
97                                 //case '/':
98                                 //      AppendBuffer (ref sb, input, start, i, @"\/");
99                                 //      break;
100                                 case '\x8':
101                                         AppendBuffer (ref sb, input, start, i, @"\b");
102                                         break;
103                                 case '\f':
104                                         AppendBuffer (ref sb, input, start, i, @"\f");
105                                         break;
106                                 case '\n':
107                                         AppendBuffer (ref sb, input, start, i, /*@"\n"*/@"\u000a");
108                                         break;
109                                 case '\r':
110                                         AppendBuffer (ref sb, input, start, i, /*@"\r"*/@"\u000d");
111                                         break;
112                                 case '\t':
113                                         AppendBuffer (ref sb, input, start, i, /*@"\t"*/@"\u0009");
114                                         break;
115                                 default:
116                                         continue;
117                                 }
118                                 start = i + 1;
119                         }
120                         string remaining = input.Substring (start, i - start);
121                         if (sb != null)
122                                 return sb.Append (remaining).ToString ();
123                         else
124                                 return remaining;
125                 }
126
127                 static void AppendBuffer (ref StringBuilder sb, string input, int start, int i, string append)
128                 {
129                         if (sb == null)
130                                 sb = new StringBuilder ();
131                         if (i != start)
132                                 sb.Append (input, start, i - start);
133                         sb.Append (append);
134                 }
135
136                 public override WriteState WriteState {
137                         get { return state; }
138                 }
139
140                 public override void Close ()
141                 {
142                         // close all open elements
143                         while (element_kinds.Count > 0)
144                                 WriteEndElement ();
145
146                         if (close_output)
147                                 output.Close ();
148                         else
149                                 output.Flush ();
150
151                         state = WriteState.Closed;
152                 }
153
154                 public override void Flush ()
155                 {
156                         output.Flush ();
157                 }
158
159                 public override void WriteStartElement (string prefix, string localName, string ns)
160                 {
161                         CheckState ();
162
163                         if (localName == null)
164                                 throw new ArgumentNullException ("localName");
165                         else if (localName.Length == 0)
166                                 throw new ArgumentException ("Empty string is not a valid localName in this XmlDictionaryWriter");
167
168                         if (!String.IsNullOrEmpty (ns))
169                                 throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter");
170                         if (!String.IsNullOrEmpty (prefix))
171                                 throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter");
172
173                         if (state == WriteState.Attribute)
174                                 WriteEndAttribute ();
175                         if (state == WriteState.Element)
176                                 CloseStartElement ();
177
178                         else if (state != WriteState.Start && element_kinds.Count == 0)
179                                 throw new XmlException ("This XmlDictionaryWriter does not support multiple top-level elements");
180
181                         if (element_kinds.Count == 0) {
182                                 if (localName != "root")
183                                         throw new XmlException ("Only 'root' is allowed for the name of the top-level element");
184                         } else {
185                                 switch (element_kinds.Peek ()) {
186                                 case ElementType.Array:
187                                         if (localName != "item")
188                                                 throw new XmlException ("Only 'item' is allowed as a content element of an array");
189                                         break;
190                                 case ElementType.String:
191                                         throw new XmlException ("Mixed content is not allowed in this XmlDictionaryWriter");
192                                 case ElementType.None:
193                                         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");
194                                 }
195
196                                 if (first_content_flags.Peek ()) {
197                                         first_content_flags.Pop ();
198                                         first_content_flags.Push (false);
199                                 }
200                                 else
201                                         OutputAsciiChar (',');
202
203                                 if (element_kinds.Peek () != ElementType.Array) {
204                                         OutputAsciiChar ('"');
205                                         OutputString (localName);
206                                         OutputAsciiChar ('\"');
207                                         OutputAsciiChar (':');
208                                 }
209                         }
210
211                         element_kinds.Push (ElementType.None); // undetermined yet
212
213                         state = WriteState.Element;
214                 }
215
216                 public override void WriteEndElement ()
217                 {
218                         CheckState ();
219
220                         if (state == WriteState.Attribute)
221                                 throw new XmlException ("Cannot end element when an attribute is being written");
222                         if (state == WriteState.Element)
223                                 CloseStartElement ();
224
225                         if (element_kinds.Count == 0)
226                                 throw new XmlException ("There is no open element to close");
227                         switch (element_kinds.Pop ()) {
228                         case ElementType.String:
229                                 if (!is_null) {
230                                         if (no_string_yet)
231                                                 OutputAsciiChar ('"');
232                                         OutputAsciiChar ('"');
233                                 }
234                                 no_string_yet = true;
235                                 is_null = false;
236                                 break;
237                         case ElementType.Array:
238                                 OutputAsciiChar (']');
239                                 break;
240                         case ElementType.Object:
241                                 OutputAsciiChar ('}');
242                                 break;
243                         }
244
245                         // not sure if it is correct though ...
246                         state = WriteState.Content;
247                         first_content_flags.Pop ();
248                 }
249
250                 public override void WriteFullEndElement ()
251                 {
252                         WriteEndElement (); // no such difference in JSON.
253                 }
254
255                 public override void WriteStartAttribute (string prefix, string localName, string ns)
256                 {
257                         CheckState ();
258
259                         if (state != WriteState.Element)
260                                 throw new XmlException ("Cannot write attribute as this XmlDictionaryWriter is not at element state");
261
262                         if (!String.IsNullOrEmpty (ns))
263                                 throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter");
264                         if (!String.IsNullOrEmpty (prefix))
265                                 throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter");
266
267                         if (localName != "type" && localName != "__type")
268                                 throw new ArgumentException ("Only 'type' and '__type' are allowed as an attribute name in this XmlDictionaryWriter");
269
270                         if (state != WriteState.Element)
271                                 throw new InvalidOperationException (String.Format ("Attribute cannot be written in {0} mode", state));
272
273                         attr_name = localName;
274                         state = WriteState.Attribute;
275                 }
276
277                 public override void WriteEndAttribute ()
278                 {
279                         CheckState ();
280
281                         if (state != WriteState.Attribute)
282                                 throw new XmlException ("Cannot close attribute, as this XmlDictionaryWriter is not at attribute state");
283
284                         if (attr_name == "type") {
285                                 switch (attr_value) {
286                                 case "object":
287                                         element_kinds.Pop ();
288                                         element_kinds.Push (ElementType.Object);
289                                         OutputAsciiChar ('{');
290                                         break;
291                                 case "array":
292                                         element_kinds.Pop ();
293                                         element_kinds.Push (ElementType.Array);
294                                         OutputAsciiChar ('[');
295                                         break;
296                                 case "number":
297                                         element_kinds.Pop ();
298                                         element_kinds.Push (ElementType.Number);
299                                         break;
300                                 case "boolean":
301                                         element_kinds.Pop ();
302                                         element_kinds.Push (ElementType.Boolean);
303                                         break;
304                                 case "string":
305                                         element_kinds.Pop ();
306                                         element_kinds.Push (ElementType.String);
307                                         break;
308                                 default:
309                                         throw new XmlException (String.Format ("Unexpected type attribute value '{0}'", attr_value));
310                                 }
311                         }
312                         else
313                                 runtime_type = attr_value;
314
315                         state = WriteState.Element;
316                         attr_value = null;
317                 }
318
319                 void CloseStartElement ()
320                 {
321                         if (element_kinds.Peek () == ElementType.None) {
322                                 element_kinds.Pop ();
323                                 element_kinds.Push (ElementType.String);
324                                 no_string_yet = true;
325                                 is_null = false;
326                         }
327
328                         first_content_flags.Push (true);
329
330                         if (runtime_type != null) {
331                                 OutputString ("\"__type\":\"");
332                                 OutputString (runtime_type);
333                                 OutputAsciiChar ('\"');
334                                 runtime_type = null;
335                                 first_content_flags.Pop ();
336                                 first_content_flags.Push (false);
337                         }
338                 }
339
340                 public override void WriteString (string text)
341                 {
342                         CheckState ();
343
344                         if (state == WriteState.Start)
345                                 throw new InvalidOperationException ("Top-level content string is not allowed in this XmlDictionaryWriter");
346
347                         if (state == WriteState.Element) {
348                                 CloseStartElement ();
349                                 state = WriteState.Content;
350                         }
351
352                         if (state == WriteState.Attribute)
353                                 attr_value += text;
354                         else if (text == null) {
355                                 no_string_yet = false;
356                                 is_null = true;
357                                 OutputString ("null");
358                         } else {
359                                 switch (element_kinds.Peek ()) {
360                                 case ElementType.String:
361                                         if (no_string_yet) {
362                                                 OutputAsciiChar ('"');
363                                                 no_string_yet = false;
364                                         }
365                                         break;
366                                 case ElementType.Number:
367                                 case ElementType.Boolean:
368                                         break;
369                                 default:
370                                         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 ()));
371                                 }
372
373                                 OutputString (EscapeStringLiteral (text));
374                         }
375                 }
376
377                 #region mostly-ignored operations
378
379                 public override string LookupPrefix (string ns)
380                 {
381                         // Since there is no way to declare namespaces in
382                         // this writer, it always returns fixed results.
383                         if (ns == null)
384                                 throw new ArgumentNullException ("ns");
385                         else if (ns.Length == 0)
386                                 return String.Empty;
387                         else if (ns == "http://www.w3.org/2000/xmlns/")
388                                 return "xmlns";
389                         else if (ns == "http://www.w3.org/XML/1998/namespace")
390                                 return "xml";
391                         return null;
392                 }
393
394                 public override void WriteStartDocument ()
395                 {
396                         CheckState ();
397                 }
398
399                 public override void WriteStartDocument (bool standalone)
400                 {
401                         CheckState ();
402                 }
403
404                 public override void WriteEndDocument ()
405                 {
406                         CheckState ();
407                 }
408
409                 #endregion
410
411                 #region unsupported operations
412
413                 public override void WriteDocType (string name, string pubid, string sysid, string intSubset)
414                 {
415                         CheckState ();
416
417                         throw new NotSupportedException ("This XmlDictionaryWriter does not support writing doctype declaration");
418                 }
419
420                 public override void WriteComment (string text)
421                 {
422                         CheckState ();
423
424                         throw new NotSupportedException ("This XmlDictionaryWriter does not support writing comment");
425                 }
426
427                 public override void WriteEntityRef (string text)
428                 {
429                         CheckState ();
430
431                         throw new NotSupportedException ("This XmlDictionaryWriter does not support writing entity reference");
432                 }
433
434                 public override void WriteProcessingInstruction (string target, string data)
435                 {
436                         CheckState ();
437
438                         if (String.Compare (target, "xml", StringComparison.OrdinalIgnoreCase) != 0)
439                                 throw new ArgumentException ("This XmlDictionaryWriter does not support writing processing instruction");
440                 }
441
442                 #endregion
443
444                 #region WriteString() variants
445
446                 public override void WriteRaw (string text)
447                 {
448                         WriteString (text);
449                 }
450
451                 public override void WriteRaw (char [] chars, int start, int length)
452                 {
453                         WriteChars (chars, start, length);
454                 }
455
456                 public override void WriteCData (string text)
457                 {
458                         WriteString (text);
459                 }
460
461                 public override void WriteCharEntity (char entity)
462                 {
463                         WriteString (entity.ToString ());
464                 }
465
466                 public override void WriteChars (char [] chars, int start, int length)
467                 {
468                         WriteString (new string (chars, start, length));
469                 }
470
471                 public override void WriteSurrogateCharEntity (char high, char low)
472                 {
473                         WriteChars (new char [] {high, low}, 0, 2);
474                 }
475
476                 public override void WriteBase64 (byte [] bytes, int start, int length)
477                 {
478                         WriteString (Convert.ToBase64String (bytes, start, length));
479                 }
480
481                 public override void WriteWhitespace (string text)
482                 {
483                         if (text == null)
484                                 throw new ArgumentNullException ("text");
485                         for (int i = 0; i < text.Length; i++) {
486                                 if (text [i] != ' ') {
487                                         for (int j = i; j < text.Length; j++) {
488                                                 switch (text [j]) {
489                                                 case '\t':
490                                                 case ' ':
491                                                 case '\n':
492                                                 case '\r':
493                                                         continue;
494                                                 default:
495                                                         throw new ArgumentException (String.Format ("WriteWhitespace() does not accept non-whitespace character '{0}'", text [j]));
496                                                 }
497                                         }
498                                         break;
499                                 }
500                         }
501                         WriteString (text);
502                 }
503
504                 void OutputAsciiChar (char c)
505                 {
506                         output.WriteByte ((byte) c);
507                 }
508
509                 void OutputString (string s)
510                 {
511                         int size = encoding.GetByteCount (s);
512                         if (encbuf.Length < size)
513                                 encbuf = new byte [size];
514                         size = encoding.GetBytes (s, 0, s.Length, encbuf, 0);
515                         output.Write (encbuf, 0, size);
516                 }
517
518                 #endregion
519         }
520 }