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
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;
56 byte [] encbuf = new byte [1024];
57 bool no_string_yet = true, is_null;
59 public JsonWriter (Stream stream, Encoding encoding, bool closeOutput)
61 SetOutput (stream, encoding, closeOutput);
64 public void SetOutput (Stream stream, Encoding encoding, bool ownsStream)
67 throw new ArgumentNullException ("stream");
69 throw new ArgumentNullException ("encoding");
71 this.encoding = encoding;
72 close_output = ownsStream;
78 case WriteState.Closed:
79 case WriteState.Error:
80 throw new InvalidOperationException (String.Format ("This XmlDictionaryReader is already at '{0}' state", state));
84 // copied from System.Silverlight JavaScriptSerializer.
85 static string EscapeStringLiteral (string input)
87 StringBuilder sb = null;
89 for (; i < input.Length; i++) {
92 AppendBuffer (ref sb, input, start, i, @"\""");
95 AppendBuffer (ref sb, input, start, i, @"\\");
98 // AppendBuffer (ref sb, input, start, i, @"\/");
101 AppendBuffer (ref sb, input, start, i, @"\b");
104 AppendBuffer (ref sb, input, start, i, @"\f");
107 AppendBuffer (ref sb, input, start, i, /*@"\n"*/@"\u000a");
110 AppendBuffer (ref sb, input, start, i, /*@"\r"*/@"\u000d");
113 AppendBuffer (ref sb, input, start, i, /*@"\t"*/@"\u0009");
120 string remaining = input.Substring (start, i - start);
122 return sb.Append (remaining).ToString ();
127 static void AppendBuffer (ref StringBuilder sb, string input, int start, int i, string append)
130 sb = new StringBuilder ();
132 sb.Append (input, start, i - start);
136 public override WriteState WriteState {
137 get { return state; }
140 public override void Close ()
142 // close all open elements
143 while (element_kinds.Count > 0)
151 state = WriteState.Closed;
154 public override void Flush ()
159 public override void WriteStartElement (string prefix, string localName, string ns)
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");
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");
173 if (state == WriteState.Attribute)
174 WriteEndAttribute ();
175 if (state == WriteState.Element)
176 CloseStartElement ();
178 else if (state != WriteState.Start && element_kinds.Count == 0)
179 throw new XmlException ("This XmlDictionaryWriter does not support multiple top-level elements");
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");
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");
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");
196 if (first_content_flags.Peek ()) {
197 first_content_flags.Pop ();
198 first_content_flags.Push (false);
201 OutputAsciiChar (',');
203 if (element_kinds.Peek () != ElementType.Array) {
204 OutputAsciiChar ('"');
205 OutputString (localName);
206 OutputAsciiChar ('\"');
207 OutputAsciiChar (':');
211 element_kinds.Push (ElementType.None); // undetermined yet
213 state = WriteState.Element;
216 public override void WriteEndElement ()
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 ();
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:
231 OutputAsciiChar ('"');
232 OutputAsciiChar ('"');
234 no_string_yet = true;
237 case ElementType.Array:
238 OutputAsciiChar (']');
240 case ElementType.Object:
241 OutputAsciiChar ('}');
245 // not sure if it is correct though ...
246 state = WriteState.Content;
247 first_content_flags.Pop ();
250 public override void WriteFullEndElement ()
252 WriteEndElement (); // no such difference in JSON.
255 public override void WriteStartAttribute (string prefix, string localName, string ns)
259 if (state != WriteState.Element)
260 throw new XmlException ("Cannot write attribute as this XmlDictionaryWriter is not at element state");
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");
267 if (localName != "type" && localName != "__type")
268 throw new ArgumentException ("Only 'type' and '__type' are allowed as an attribute name in this XmlDictionaryWriter");
270 if (state != WriteState.Element)
271 throw new InvalidOperationException (String.Format ("Attribute cannot be written in {0} mode", state));
273 attr_name = localName;
274 state = WriteState.Attribute;
277 public override void WriteEndAttribute ()
281 if (state != WriteState.Attribute)
282 throw new XmlException ("Cannot close attribute, as this XmlDictionaryWriter is not at attribute state");
284 if (attr_name == "type") {
285 switch (attr_value) {
287 element_kinds.Pop ();
288 element_kinds.Push (ElementType.Object);
289 OutputAsciiChar ('{');
292 element_kinds.Pop ();
293 element_kinds.Push (ElementType.Array);
294 OutputAsciiChar ('[');
297 element_kinds.Pop ();
298 element_kinds.Push (ElementType.Number);
301 element_kinds.Pop ();
302 element_kinds.Push (ElementType.Boolean);
305 element_kinds.Pop ();
306 element_kinds.Push (ElementType.String);
309 throw new XmlException (String.Format ("Unexpected type attribute value '{0}'", attr_value));
313 runtime_type = attr_value;
315 state = WriteState.Element;
319 void CloseStartElement ()
321 if (element_kinds.Peek () == ElementType.None) {
322 element_kinds.Pop ();
323 element_kinds.Push (ElementType.String);
324 no_string_yet = true;
328 first_content_flags.Push (true);
330 if (runtime_type != null) {
331 OutputString ("\"__type\":\"");
332 OutputString (runtime_type);
333 OutputAsciiChar ('\"');
335 first_content_flags.Pop ();
336 first_content_flags.Push (false);
340 public override void WriteString (string text)
344 if (state == WriteState.Start)
345 throw new InvalidOperationException ("Top-level content string is not allowed in this XmlDictionaryWriter");
347 if (state == WriteState.Element) {
348 CloseStartElement ();
349 state = WriteState.Content;
352 if (state == WriteState.Attribute)
354 else if (text == null) {
355 no_string_yet = false;
357 OutputString ("null");
359 switch (element_kinds.Peek ()) {
360 case ElementType.String:
362 OutputAsciiChar ('"');
363 no_string_yet = false;
366 case ElementType.Number:
367 case ElementType.Boolean:
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 ()));
373 OutputString (EscapeStringLiteral (text));
377 #region mostly-ignored operations
379 public override string LookupPrefix (string ns)
381 // Since there is no way to declare namespaces in
382 // this writer, it always returns fixed results.
384 throw new ArgumentNullException ("ns");
385 else if (ns.Length == 0)
387 else if (ns == "http://www.w3.org/2000/xmlns/")
389 else if (ns == "http://www.w3.org/XML/1998/namespace")
394 public override void WriteStartDocument ()
399 public override void WriteStartDocument (bool standalone)
404 public override void WriteEndDocument ()
411 #region unsupported operations
413 public override void WriteDocType (string name, string pubid, string sysid, string intSubset)
417 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing doctype declaration");
420 public override void WriteComment (string text)
424 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing comment");
427 public override void WriteEntityRef (string text)
431 throw new NotSupportedException ("This XmlDictionaryWriter does not support writing entity reference");
434 public override void WriteProcessingInstruction (string target, string data)
438 if (String.Compare (target, "xml", StringComparison.OrdinalIgnoreCase) != 0)
439 throw new ArgumentException ("This XmlDictionaryWriter does not support writing processing instruction");
444 #region WriteString() variants
446 public override void WriteRaw (string text)
451 public override void WriteRaw (char [] chars, int start, int length)
453 WriteChars (chars, start, length);
456 public override void WriteCData (string text)
461 public override void WriteCharEntity (char entity)
463 WriteString (entity.ToString ());
466 public override void WriteChars (char [] chars, int start, int length)
468 WriteString (new string (chars, start, length));
471 public override void WriteSurrogateCharEntity (char high, char low)
473 WriteChars (new char [] {high, low}, 0, 2);
476 public override void WriteBase64 (byte [] bytes, int start, int length)
478 WriteString (Convert.ToBase64String (bytes, start, length));
481 public override void WriteWhitespace (string text)
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++) {
495 throw new ArgumentException (String.Format ("WriteWhitespace() does not accept non-whitespace character '{0}'", text [j]));
504 void OutputAsciiChar (char c)
506 output.WriteByte ((byte) c);
509 void OutputString (string s)
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);