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;
75 is_ascii_single = encoding is UTF8Encoding;
77 is_ascii_single = encoding is UTF8Encoding || encoding.IsSingleByte;
84 case WriteState.Closed:
85 case WriteState.Error:
86 throw new InvalidOperationException (String.Format ("This XmlDictionaryReader is already at '{0}' state", state));
90 // copied from System.Silverlight JavaScriptSerializer.
91 static string EscapeStringLiteral (string input)
93 StringBuilder sb = null;
95 for (; i < input.Length; i++) {
98 AppendBuffer (ref sb, input, start, i, @"\""");
101 AppendBuffer (ref sb, input, start, i, @"\\");
104 AppendBuffer (ref sb, input, start, i, @"\/");
107 AppendBuffer (ref sb, input, start, i, @"\b");
110 AppendBuffer (ref sb, input, start, i, @"\f");
113 AppendBuffer (ref sb, input, start, i, /*@"\n"*/@"\u000a");
116 AppendBuffer (ref sb, input, start, i, /*@"\r"*/@"\u000d");
119 AppendBuffer (ref sb, input, start, i, /*@"\t"*/@"\u0009");
126 string remaining = input.Substring (start, i - start);
128 return sb.Append (remaining).ToString ();
133 static void AppendBuffer (ref StringBuilder sb, string input, int start, int i, string append)
136 sb = new StringBuilder ();
138 sb.Append (input, start, i - start);
142 public override WriteState WriteState {
143 get { return state; }
146 public override void Close ()
148 // close all open elements
149 while (element_kinds.Count > 0)
157 state = WriteState.Closed;
160 public override void Flush ()
165 public override void WriteStartElement (string prefix, string localName, string ns)
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");
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");
179 if (state == WriteState.Attribute)
180 WriteEndAttribute ();
181 if (state == WriteState.Element)
182 CloseStartElement ();
184 else if (state != WriteState.Start && element_kinds.Count == 0)
185 throw new XmlException ("This XmlDictionaryWriter does not support multiple top-level elements");
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");
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");
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");
204 if (first_content_flags.Peek ()) {
205 first_content_flags.Pop ();
206 first_content_flags.Push (false);
209 OutputAsciiChar (',');
211 if (element_kinds.Peek () != ElementType.Array) {
212 OutputAsciiChar ('"');
213 OutputString (localName);
214 OutputAsciiChar ('\"');
215 OutputAsciiChar (':');
219 element_kinds.Push (ElementType.None); // undetermined yet
221 state = WriteState.Element;
224 public override void WriteEndElement ()
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 ();
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:
239 OutputAsciiChar ('"');
240 OutputAsciiChar ('"');
242 no_string_yet = true;
245 case ElementType.Array:
246 OutputAsciiChar (']');
248 case ElementType.Object:
249 OutputAsciiChar ('}');
253 // not sure if it is correct though ...
254 state = WriteState.Content;
255 first_content_flags.Pop ();
258 public override void WriteFullEndElement ()
260 WriteEndElement (); // no such difference in JSON.
263 public override void WriteStartAttribute (string prefix, string localName, string ns)
267 if (state != WriteState.Element)
268 throw new XmlException ("Cannot write attribute as this XmlDictionaryWriter is not at element state");
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");
275 if (localName != "type" && localName != "__type")
276 throw new ArgumentException ("Only 'type' and '__type' are allowed as an attribute name in this XmlDictionaryWriter");
278 if (state != WriteState.Element)
279 throw new InvalidOperationException (String.Format ("Attribute cannot be written in {0} mode", state));
281 attr_name = localName;
282 state = WriteState.Attribute;
285 public override void WriteEndAttribute ()
289 if (state != WriteState.Attribute)
290 throw new XmlException ("Cannot close attribute, as this XmlDictionaryWriter is not at attribute state");
292 if (attr_name == "type") {
293 switch (attr_value) {
295 element_kinds.Pop ();
296 element_kinds.Push (ElementType.Object);
297 OutputAsciiChar ('{');
300 element_kinds.Pop ();
301 element_kinds.Push (ElementType.Array);
302 OutputAsciiChar ('[');
305 element_kinds.Pop ();
306 element_kinds.Push (ElementType.Number);
309 element_kinds.Pop ();
310 element_kinds.Push (ElementType.Boolean);
313 element_kinds.Pop ();
314 element_kinds.Push (ElementType.String);
317 element_kinds.Pop ();
318 element_kinds.Push (ElementType.Null);
319 OutputString ("null");
322 throw new XmlException (String.Format ("Unexpected type attribute value '{0}'", attr_value));
326 runtime_type = attr_value;
328 state = WriteState.Element;
332 void CloseStartElement ()
334 if (element_kinds.Peek () == ElementType.None) {
335 element_kinds.Pop ();
336 element_kinds.Push (ElementType.String);
337 no_string_yet = true;
341 first_content_flags.Push (true);
343 if (runtime_type != null) {
344 OutputString ("\"__type\":\"");
345 OutputString (runtime_type);
346 OutputAsciiChar ('\"');
348 first_content_flags.Pop ();
349 first_content_flags.Push (false);
353 public override void WriteString (string text)
357 if (state == WriteState.Start)
358 throw new InvalidOperationException ("Top-level content string is not allowed in this XmlDictionaryWriter");
360 if (state == WriteState.Element) {
361 CloseStartElement ();
362 state = WriteState.Content;
365 if (state == WriteState.Attribute)
367 else if (text == null) {
368 no_string_yet = false;
370 if (element_kinds.Peek () != ElementType.Null)
371 OutputString ("null");
373 switch (element_kinds.Peek ()) {
374 case ElementType.String:
376 OutputAsciiChar ('"');
377 no_string_yet = false;
380 case ElementType.Number:
381 // .NET is buggy here, it just outputs raw string, which results in invalid JSON format.
382 bool isString = false;
391 element_kinds.Pop ();
392 element_kinds.Push (ElementType.String);
393 goto case ElementType.String;
396 case ElementType.Boolean:
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 ()));
402 OutputString (EscapeStringLiteral (text));
406 #region mostly-ignored operations
408 public override string LookupPrefix (string ns)
410 // Since there is no way to declare namespaces in
411 // this writer, it always returns fixed results.
413 throw new ArgumentNullException ("ns");
414 else if (ns.Length == 0)
416 else if (ns == "http://www.w3.org/2000/xmlns/")
418 else if (ns == "http://www.w3.org/XML/1998/namespace")
423 public override void WriteStartDocument ()
428 public override void WriteStartDocument (bool standalone)
433 public override void WriteEndDocument ()
440 #region unsupported operations
442 public override void WriteDocType (string name, string pubid, string sysid, string intSubset)
446 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing doctype declaration");
449 public override void WriteComment (string text)
453 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing comment");
456 public override void WriteEntityRef (string text)
460 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing entity reference");
463 public override void WriteProcessingInstruction (string target, string data)
467 if (String.Compare (target, "xml", StringComparison.OrdinalIgnoreCase) != 0)
468 throw new ArgumentException ("This XmlDictionaryWriter does not support writing processing instruction");
473 #region WriteString() variants
475 public override void WriteRaw (string text)
484 public override void WriteRaw (char [] chars, int start, int length)
486 WriteChars (chars, start, length);
489 public override void WriteCData (string text)
494 public override void WriteCharEntity (char entity)
496 WriteString (entity.ToString ());
499 public override void WriteChars (char [] chars, int start, int length)
501 WriteString (new string (chars, start, length));
504 public override void WriteSurrogateCharEntity (char high, char low)
506 WriteChars (new char [] {high, low}, 0, 2);
509 public override void WriteBase64 (byte [] bytes, int start, int length)
511 WriteString (Convert.ToBase64String (bytes, start, length));
514 public override void WriteWhitespace (string text)
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++) {
528 throw new ArgumentException (String.Format ("WriteWhitespace() does not accept non-whitespace character '{0}'", text [j]));
537 char [] char_buf = new char [1];
538 void OutputAsciiChar (char c)
541 output.WriteByte ((byte) c);
544 int size = encoding.GetBytes (char_buf, 0, 1, encbuf, 0);
545 output.Write (encbuf, 0, size);
549 void OutputString (string s)
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);