// // JsonWriter.cs // // Author: // Atsushi Enomoto // // Copyright (C) 2007 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Xml; namespace System.Runtime.Serialization.Json { class JsonWriter : XmlDictionaryWriter, IXmlJsonWriterInitializer { enum ElementType { None, Object, Array, String, Number, Boolean, } Stream output; bool close_output; WriteState state; Stack element_kinds = new Stack (); Stack first_content_flags = new Stack (); string attr_name, attr_value, runtime_type; Encoding encoding; byte [] encbuf = new byte [1024]; bool no_string_yet = true, is_null; public JsonWriter (Stream stream, Encoding encoding, bool closeOutput) { SetOutput (stream, encoding, closeOutput); } public void SetOutput (Stream stream, Encoding encoding, bool ownsStream) { if (stream == null) throw new ArgumentNullException ("stream"); if (encoding == null) throw new ArgumentNullException ("encoding"); output = stream; this.encoding = encoding; close_output = ownsStream; } void CheckState () { switch (state) { case WriteState.Closed: case WriteState.Error: throw new InvalidOperationException (String.Format ("This XmlDictionaryReader is already at '{0}' state", state)); } } // copied from System.Silverlight JavaScriptSerializer. static string EscapeStringLiteral (string input) { StringBuilder sb = null; int i = 0, start = 0; for (; i < input.Length; i++) { switch (input [i]) { case '"': AppendBuffer (ref sb, input, start, i, @"\"""); break; case '\\': AppendBuffer (ref sb, input, start, i, @"\\"); break; //case '/': // AppendBuffer (ref sb, input, start, i, @"\/"); // break; case '\x8': AppendBuffer (ref sb, input, start, i, @"\b"); break; case '\f': AppendBuffer (ref sb, input, start, i, @"\f"); break; case '\n': AppendBuffer (ref sb, input, start, i, /*@"\n"*/@"\u000a"); break; case '\r': AppendBuffer (ref sb, input, start, i, /*@"\r"*/@"\u000d"); break; case '\t': AppendBuffer (ref sb, input, start, i, /*@"\t"*/@"\u0009"); break; default: continue; } start = i + 1; } string remaining = input.Substring (start, i - start); if (sb != null) return sb.Append (remaining).ToString (); else return remaining; } static void AppendBuffer (ref StringBuilder sb, string input, int start, int i, string append) { if (sb == null) sb = new StringBuilder (); if (i != start) sb.Append (input, start, i - start); sb.Append (append); } public override WriteState WriteState { get { return state; } } public override void Close () { // close all open elements while (element_kinds.Count > 0) WriteEndElement (); if (close_output) output.Close (); else output.Flush (); state = WriteState.Closed; } public override void Flush () { output.Flush (); } public override void WriteStartElement (string prefix, string localName, string ns) { CheckState (); if (localName == null) throw new ArgumentNullException ("localName"); else if (localName.Length == 0) throw new ArgumentException ("Empty string is not a valid localName in this XmlDictionaryWriter"); if (!String.IsNullOrEmpty (ns)) throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter"); if (!String.IsNullOrEmpty (prefix)) throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter"); if (state == WriteState.Attribute) WriteEndAttribute (); if (state == WriteState.Element) CloseStartElement (); else if (state != WriteState.Start && element_kinds.Count == 0) throw new XmlException ("This XmlDictionaryWriter does not support multiple top-level elements"); if (element_kinds.Count == 0) { if (localName != "root") throw new XmlException ("Only 'root' is allowed for the name of the top-level element"); } else { switch (element_kinds.Peek ()) { case ElementType.Array: if (localName != "item") throw new XmlException ("Only 'item' is allowed as a content element of an array"); break; case ElementType.String: throw new XmlException ("Mixed content is not allowed in this XmlDictionaryWriter"); case ElementType.None: 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"); } if (first_content_flags.Peek ()) { first_content_flags.Pop (); first_content_flags.Push (false); } else OutputAsciiChar (','); if (element_kinds.Peek () != ElementType.Array) { OutputAsciiChar ('"'); OutputString (localName); OutputAsciiChar ('\"'); OutputAsciiChar (':'); } } element_kinds.Push (ElementType.None); // undetermined yet state = WriteState.Element; } public override void WriteEndElement () { CheckState (); if (state == WriteState.Attribute) throw new XmlException ("Cannot end element when an attribute is being written"); if (state == WriteState.Element) CloseStartElement (); if (element_kinds.Count == 0) throw new XmlException ("There is no open element to close"); switch (element_kinds.Pop ()) { case ElementType.String: if (!is_null) { if (no_string_yet) OutputAsciiChar ('"'); OutputAsciiChar ('"'); } no_string_yet = true; is_null = false; break; case ElementType.Array: OutputAsciiChar (']'); break; case ElementType.Object: OutputAsciiChar ('}'); break; } // not sure if it is correct though ... state = WriteState.Content; first_content_flags.Pop (); } public override void WriteFullEndElement () { WriteEndElement (); // no such difference in JSON. } public override void WriteStartAttribute (string prefix, string localName, string ns) { CheckState (); if (state != WriteState.Element) throw new XmlException ("Cannot write attribute as this XmlDictionaryWriter is not at element state"); if (!String.IsNullOrEmpty (ns)) throw new ArgumentException ("Non-empty namespace URI is not allowed in this XmlDictionaryWriter"); if (!String.IsNullOrEmpty (prefix)) throw new ArgumentException ("Non-empty prefix is not allowed in this XmlDictionaryWriter"); if (localName != "type" && localName != "__type") throw new ArgumentException ("Only 'type' and '__type' are allowed as an attribute name in this XmlDictionaryWriter"); if (state != WriteState.Element) throw new InvalidOperationException (String.Format ("Attribute cannot be written in {0} mode", state)); attr_name = localName; state = WriteState.Attribute; } public override void WriteEndAttribute () { CheckState (); if (state != WriteState.Attribute) throw new XmlException ("Cannot close attribute, as this XmlDictionaryWriter is not at attribute state"); if (attr_name == "type") { switch (attr_value) { case "object": element_kinds.Pop (); element_kinds.Push (ElementType.Object); OutputAsciiChar ('{'); break; case "array": element_kinds.Pop (); element_kinds.Push (ElementType.Array); OutputAsciiChar ('['); break; case "number": element_kinds.Pop (); element_kinds.Push (ElementType.Number); break; case "boolean": element_kinds.Pop (); element_kinds.Push (ElementType.Boolean); break; case "string": element_kinds.Pop (); element_kinds.Push (ElementType.String); break; default: throw new XmlException (String.Format ("Unexpected type attribute value '{0}'", attr_value)); } } else runtime_type = attr_value; state = WriteState.Element; attr_value = null; } void CloseStartElement () { if (element_kinds.Peek () == ElementType.None) { element_kinds.Pop (); element_kinds.Push (ElementType.String); no_string_yet = true; is_null = false; } first_content_flags.Push (true); if (runtime_type != null) { OutputString ("\"__type\":\""); OutputString (runtime_type); OutputAsciiChar ('\"'); runtime_type = null; first_content_flags.Pop (); first_content_flags.Push (false); } } public override void WriteString (string text) { CheckState (); if (state == WriteState.Start) throw new InvalidOperationException ("Top-level content string is not allowed in this XmlDictionaryWriter"); if (state == WriteState.Element) { CloseStartElement (); state = WriteState.Content; } if (state == WriteState.Attribute) attr_value += text; else if (text == null) { no_string_yet = false; is_null = true; OutputString ("null"); } else { switch (element_kinds.Peek ()) { case ElementType.String: if (no_string_yet) { OutputAsciiChar ('"'); no_string_yet = false; } break; case ElementType.Number: case ElementType.Boolean: break; default: 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 ())); } OutputString (EscapeStringLiteral (text)); } } #region mostly-ignored operations public override string LookupPrefix (string ns) { // Since there is no way to declare namespaces in // this writer, it always returns fixed results. if (ns == null) throw new ArgumentNullException ("ns"); else if (ns.Length == 0) return String.Empty; else if (ns == "http://www.w3.org/2000/xmlns/") return "xmlns"; else if (ns == "http://www.w3.org/XML/1998/namespace") return "xml"; return null; } public override void WriteStartDocument () { CheckState (); } public override void WriteStartDocument (bool standalone) { CheckState (); } public override void WriteEndDocument () { CheckState (); } #endregion #region unsupported operations public override void WriteDocType (string name, string pubid, string sysid, string intSubset) { CheckState (); throw new NotSupportedException ("This XmlDictionaryWriter does not support writing doctype declaration"); } public override void WriteComment (string text) { CheckState (); throw new NotSupportedException ("This XmlDictionaryWriter does not support writing comment"); } public override void WriteEntityRef (string text) { CheckState (); throw new NotSupportedException ("This XmlDictionaryWriter does not support writing entity reference"); } public override void WriteProcessingInstruction (string target, string data) { CheckState (); if (String.Compare (target, "xml", StringComparison.OrdinalIgnoreCase) != 0) throw new ArgumentException ("This XmlDictionaryWriter does not support writing processing instruction"); } #endregion #region WriteString() variants public override void WriteRaw (string text) { WriteString (text); } public override void WriteRaw (char [] chars, int start, int length) { WriteChars (chars, start, length); } public override void WriteCData (string text) { WriteString (text); } public override void WriteCharEntity (char entity) { WriteString (entity.ToString ()); } public override void WriteChars (char [] chars, int start, int length) { WriteString (new string (chars, start, length)); } public override void WriteSurrogateCharEntity (char high, char low) { WriteChars (new char [] {high, low}, 0, 2); } public override void WriteBase64 (byte [] bytes, int start, int length) { WriteString (Convert.ToBase64String (bytes, start, length)); } public override void WriteWhitespace (string text) { if (text == null) throw new ArgumentNullException ("text"); for (int i = 0; i < text.Length; i++) { if (text [i] != ' ') { for (int j = i; j < text.Length; j++) { switch (text [j]) { case '\t': case ' ': case '\n': case '\r': continue; default: throw new ArgumentException (String.Format ("WriteWhitespace() does not accept non-whitespace character '{0}'", text [j])); } } break; } } WriteString (text); } void OutputAsciiChar (char c) { output.WriteByte ((byte) c); } void OutputString (string s) { int size = encoding.GetByteCount (s); if (encbuf.Length < size) encbuf = new byte [size]; size = encoding.GetBytes (s, 0, s.Length, encbuf, 0); output.Write (encbuf, 0, size); } #endregion } }