5 // Atsushi Enomoto <atsushi@ximian.com>
7 // Copyright (C) 2007 Novell, Inc (http://www.novell.com)
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:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
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.
29 using System.Collections.Generic;
30 using System.Globalization;
35 namespace System.Runtime.Serialization.Json
37 class JsonWriter : XmlDictionaryWriter, IXmlJsonWriterInitializer
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;
57 byte [] encbuf = new byte [1024];
58 bool no_string_yet = true, is_null, is_ascii_single;
60 public JsonWriter (Stream stream, Encoding encoding, bool closeOutput)
62 SetOutput (stream, encoding, closeOutput);
65 public void SetOutput (Stream stream, Encoding encoding, bool ownsStream)
68 throw new ArgumentNullException ("stream");
70 throw new ArgumentNullException ("encoding");
72 this.encoding = encoding;
73 close_output = ownsStream;
74 is_ascii_single = encoding is UTF8Encoding || encoding.IsSingleByte;
80 case WriteState.Closed:
81 case WriteState.Error:
82 throw new InvalidOperationException (String.Format ("This XmlDictionaryReader is already at '{0}' state", state));
86 // copied from System.Silverlight JavaScriptSerializer.
87 static string EscapeStringLiteral (string input)
89 StringBuilder sb = null;
91 for (; i < input.Length; i++) {
94 AppendBuffer (ref sb, input, start, i, @"\""");
97 AppendBuffer (ref sb, input, start, i, @"\\");
100 AppendBuffer (ref sb, input, start, i, @"\/");
103 AppendBuffer (ref sb, input, start, i, @"\b");
106 AppendBuffer (ref sb, input, start, i, @"\f");
109 AppendBuffer (ref sb, input, start, i, /*@"\n"*/@"\u000a");
112 AppendBuffer (ref sb, input, start, i, /*@"\r"*/@"\u000d");
115 AppendBuffer (ref sb, input, start, i, /*@"\t"*/@"\u0009");
122 string remaining = input.Substring (start, i - start);
124 return sb.Append (remaining).ToString ();
129 static void AppendBuffer (ref StringBuilder sb, string input, int start, int i, string append)
132 sb = new StringBuilder ();
134 sb.Append (input, start, i - start);
138 public override WriteState WriteState {
139 get { return state; }
142 public override void Close ()
144 // close all open elements
145 while (element_kinds.Count > 0)
153 state = WriteState.Closed;
156 public override void Flush ()
161 public override void WriteStartElement (string prefix, string localName, string ns)
165 if (localName == null)
166 throw new ArgumentNullException ("localName");
167 else if (localName.Length == 0)
168 throw new ArgumentException ("Empty string is not a valid localName in this XmlDictionaryWriter");
170 if (!String.IsNullOrEmpty (ns))
171 throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter");
172 if (!String.IsNullOrEmpty (prefix))
173 throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter");
175 if (state == WriteState.Attribute)
176 WriteEndAttribute ();
177 if (state == WriteState.Element)
178 CloseStartElement ();
180 else if (state != WriteState.Start && element_kinds.Count == 0)
181 throw new XmlException ("This XmlDictionaryWriter does not support multiple top-level elements");
183 if (element_kinds.Count == 0) {
184 if (localName != "root")
185 throw new XmlException ("Only 'root' is allowed for the name of the top-level element");
187 switch (element_kinds.Peek ()) {
188 case ElementType.Array:
189 if (localName != "item")
190 throw new XmlException ("Only 'item' is allowed as a content element of an array");
192 case ElementType.String:
193 throw new XmlException ("Mixed content is not allowed in this XmlDictionaryWriter");
194 case ElementType.Null:
195 throw new XmlException ("Current type is null and writing element inside null is not allowed");
196 case ElementType.None:
197 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");
200 if (first_content_flags.Peek ()) {
201 first_content_flags.Pop ();
202 first_content_flags.Push (false);
205 OutputAsciiChar (',');
207 if (element_kinds.Peek () != ElementType.Array) {
208 OutputAsciiChar ('"');
209 OutputString (localName);
210 OutputAsciiChar ('\"');
211 OutputAsciiChar (':');
215 element_kinds.Push (ElementType.None); // undetermined yet
217 state = WriteState.Element;
220 public override void WriteEndElement ()
224 if (state == WriteState.Attribute)
225 throw new XmlException ("Cannot end element when an attribute is being written");
226 if (state == WriteState.Element)
227 CloseStartElement ();
229 if (element_kinds.Count == 0)
230 throw new XmlException ("There is no open element to close");
231 switch (element_kinds.Pop ()) {
232 case ElementType.String:
235 OutputAsciiChar ('"');
236 OutputAsciiChar ('"');
238 no_string_yet = true;
241 case ElementType.Array:
242 OutputAsciiChar (']');
244 case ElementType.Object:
245 OutputAsciiChar ('}');
249 // not sure if it is correct though ...
250 state = WriteState.Content;
251 first_content_flags.Pop ();
254 public override void WriteFullEndElement ()
256 WriteEndElement (); // no such difference in JSON.
259 public override void WriteStartAttribute (string prefix, string localName, string ns)
263 if (state != WriteState.Element)
264 throw new XmlException ("Cannot write attribute as this XmlDictionaryWriter is not at element state");
266 if (!String.IsNullOrEmpty (ns))
267 throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter");
268 if (!String.IsNullOrEmpty (prefix))
269 throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter");
271 if (localName != "type" && localName != "__type")
272 throw new ArgumentException ("Only 'type' and '__type' are allowed as an attribute name in this XmlDictionaryWriter");
274 if (state != WriteState.Element)
275 throw new InvalidOperationException (String.Format ("Attribute cannot be written in {0} mode", state));
277 attr_name = localName;
278 state = WriteState.Attribute;
281 public override void WriteEndAttribute ()
285 if (state != WriteState.Attribute)
286 throw new XmlException ("Cannot close attribute, as this XmlDictionaryWriter is not at attribute state");
288 if (attr_name == "type") {
289 switch (attr_value) {
291 element_kinds.Pop ();
292 element_kinds.Push (ElementType.Object);
293 OutputAsciiChar ('{');
296 element_kinds.Pop ();
297 element_kinds.Push (ElementType.Array);
298 OutputAsciiChar ('[');
301 element_kinds.Pop ();
302 element_kinds.Push (ElementType.Number);
305 element_kinds.Pop ();
306 element_kinds.Push (ElementType.Boolean);
309 element_kinds.Pop ();
310 element_kinds.Push (ElementType.String);
313 element_kinds.Pop ();
314 element_kinds.Push (ElementType.Null);
315 OutputString ("null");
318 throw new XmlException (String.Format ("Unexpected type attribute value '{0}'", attr_value));
322 runtime_type = attr_value;
324 state = WriteState.Element;
328 void CloseStartElement ()
330 if (element_kinds.Peek () == ElementType.None) {
331 element_kinds.Pop ();
332 element_kinds.Push (ElementType.String);
333 no_string_yet = true;
337 first_content_flags.Push (true);
339 if (runtime_type != null) {
340 OutputString ("\"__type\":\"");
341 OutputString (runtime_type);
342 OutputAsciiChar ('\"');
344 first_content_flags.Pop ();
345 first_content_flags.Push (false);
349 public override void WriteString (string text)
353 if (state == WriteState.Start)
354 throw new InvalidOperationException ("Top-level content string is not allowed in this XmlDictionaryWriter");
356 if (state == WriteState.Element) {
357 CloseStartElement ();
358 state = WriteState.Content;
361 if (state == WriteState.Attribute)
363 else if (text == null) {
364 no_string_yet = false;
366 if (element_kinds.Peek () != ElementType.Null)
367 OutputString ("null");
369 switch (element_kinds.Peek ()) {
370 case ElementType.String:
372 OutputAsciiChar ('"');
373 no_string_yet = false;
376 case ElementType.Number:
377 // .NET is buggy here, it just outputs raw string, which results in invalid JSON format.
378 bool isString = false;
387 element_kinds.Pop ();
388 element_kinds.Push (ElementType.String);
389 goto case ElementType.String;
392 case ElementType.Boolean:
395 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 ()));
398 OutputString (EscapeStringLiteral (text));
402 #region mostly-ignored operations
404 public override string LookupPrefix (string ns)
406 // Since there is no way to declare namespaces in
407 // this writer, it always returns fixed results.
409 throw new ArgumentNullException ("ns");
410 else if (ns.Length == 0)
412 else if (ns == "http://www.w3.org/2000/xmlns/")
414 else if (ns == "http://www.w3.org/XML/1998/namespace")
419 public override void WriteStartDocument ()
424 public override void WriteStartDocument (bool standalone)
429 public override void WriteEndDocument ()
436 #region unsupported operations
438 public override void WriteDocType (string name, string pubid, string sysid, string intSubset)
442 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing doctype declaration");
445 public override void WriteComment (string text)
449 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing comment");
452 public override void WriteEntityRef (string text)
456 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing entity reference");
459 public override void WriteProcessingInstruction (string target, string data)
463 if (String.Compare (target, "xml", StringComparison.OrdinalIgnoreCase) != 0)
464 throw new ArgumentException ("This XmlDictionaryWriter does not support writing processing instruction");
469 #region WriteString() variants
471 public override void WriteRaw (string text)
476 public override void WriteRaw (char [] chars, int start, int length)
478 WriteChars (chars, start, length);
481 public override void WriteCData (string text)
486 public override void WriteCharEntity (char entity)
488 WriteString (entity.ToString ());
491 public override void WriteChars (char [] chars, int start, int length)
493 WriteString (new string (chars, start, length));
496 public override void WriteSurrogateCharEntity (char high, char low)
498 WriteChars (new char [] {high, low}, 0, 2);
501 public override void WriteBase64 (byte [] bytes, int start, int length)
503 WriteString (Convert.ToBase64String (bytes, start, length));
506 public override void WriteWhitespace (string text)
509 throw new ArgumentNullException ("text");
510 for (int i = 0; i < text.Length; i++) {
511 if (text [i] != ' ') {
512 for (int j = i; j < text.Length; j++) {
520 throw new ArgumentException (String.Format ("WriteWhitespace() does not accept non-whitespace character '{0}'", text [j]));
529 char [] char_buf = new char [1];
530 void OutputAsciiChar (char c)
533 output.WriteByte ((byte) c);
536 int size = encoding.GetBytes (char_buf, 0, 1, encbuf, 0);
537 output.Write (encbuf, 0, size);
541 void OutputString (string s)
543 int size = encoding.GetByteCount (s);
544 if (encbuf.Length < size)
545 encbuf = new byte [size];
546 size = encoding.GetBytes (s, 0, s.Length, encbuf, 0);
547 output.Write (encbuf, 0, size);