File: System\Data\Services\Serializers\JsonWriter.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="JsonWriter.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides a writer implementaion for Json format
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Serializers
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Text;
    using System.Xml;
 
    /// <summary>
    /// Json text writer
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Original writer is disposed of?")]
    internal sealed class JsonWriter
    {
        /// <summary> const tick value for caculating tick values</summary>
        internal static readonly long DatetimeMinTimeTicks = (new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).Ticks;
        
        /// <summary> Json datetime format </summary>
        private const string JsonDateTimeFormat = @"\/Date({0})\/";
 
        /// <summary>Text used to start a data object wrapper in JSON.</summary>
        private const string JsonDataWrapper = "\"d\" : ";
 
        /// <summary>"results" header for Json data array</summary>
        private const string JsonResultName = "results";
 
        /// <summary> Writer to write text into </summary>
        private readonly IndentedTextWriter writer;
 
        /// <summary> scope of the json text - object, array, etc</summary>
        private readonly Stack<Scope> scopes;
 
        /// <summary>
        /// Creates a new instance of Json writer
        /// </summary>
        /// <param name="writer">writer to which text needs to be written</param>
        public JsonWriter(TextWriter writer)
        {
            this.writer = new IndentedTextWriter(writer);
            this.scopes = new Stack<Scope>();
        }
 
        /// <summary>
        /// Various scope types for Json writer
        /// </summary>
        private enum ScopeType
        {
            /// <summary> array scope </summary>
            Array = 0,
 
            /// <summary> object scope</summary>
            Object = 1
        }
 
        /// <summary>
        /// End the current scope
        /// </summary>
        public void EndScope()
        {
            if (this.scopes.Count == 0)
            {
                throw new InvalidOperationException("No active scope to end.");
            }
 
            this.writer.WriteLine();
            this.writer.Indent--;
 
            Scope scope = this.scopes.Pop();
            if (scope.Type == ScopeType.Array)
            {
                this.writer.Write("]");
            }
            else
            {
                this.writer.Write("}");
            }
        }
 
        /// <summary>
        /// Start the array scope
        /// </summary>
        public void StartArrayScope()
        {
            this.StartScope(ScopeType.Array);
        }
 
        /// <summary>
        /// Write the "d" wrapper text
        /// </summary>
        public void WriteDataWrapper()
        {
            this.writer.Write(JsonDataWrapper);
        }
 
        /// <summary>
        /// Write the "results" header for the data array
        /// </summary>
        public void WriteDataArrayName()
        {
            this.WriteName(JsonResultName);
        }
 
        /// <summary>
        /// Start the object scope
        /// </summary>
        public void StartObjectScope()
        {
            this.StartScope(ScopeType.Object);
        }
 
        /// <summary>
        /// Write the name for the object property
        /// </summary>
        /// <param name="name">name of the object property </param>
        public void WriteName(string name)
        {
            if (String.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
 
            if (this.scopes.Count == 0)
            {
                throw new InvalidOperationException("No active scope to write into.");
            }
 
            if (this.scopes.Peek().Type != ScopeType.Object)
            {
                throw new InvalidOperationException("Names can only be written into Object Scopes.");
            }
 
            Scope currentScope = this.scopes.Peek();
            if (currentScope.Type == ScopeType.Object)
            {
                if (currentScope.ObjectCount != 0)
                {
                    this.writer.WriteTrimmed(", ");
                }
 
                currentScope.ObjectCount++;
            }
 
            this.WriteCore(QuoteJScriptString(name), true /*quotes*/);
            this.writer.WriteTrimmed(": ");
        }
 
        /// <summary>
        /// Write the bool value
        /// </summary>
        /// <param name="value">bool value to be written</param>
        public void WriteValue(bool value)
        {
            this.WriteCore(value ? XmlConstants.XmlTrueLiteral : XmlConstants.XmlFalseLiteral, /* quotes */ false);
        }
 
        /// <summary>
        /// Write the int value
        /// </summary>
        /// <param name="value">int value to be written</param>
        public void WriteValue(int value)
        {
            this.WriteCore(value.ToString(CultureInfo.InvariantCulture), /* quotes */ false);
        }
 
        /// <summary>
        /// Write the float value
        /// </summary>
        /// <param name="value">float value to be written</param>
        public void WriteValue(float value)
        {
            if (double.IsInfinity(value) || double.IsNaN(value))
            {
                this.WriteCore(value.ToString(null, CultureInfo.InvariantCulture), true /*quotes*/);
            }
            else
            {
                // float.ToString() supports a max scale of six,
                // whereas float.MinValue and float.MaxValue have 8 digits scale. Hence we need
                // to use XmlConvert in all other cases, except infinity
                this.WriteCore(XmlConvert.ToString(value), /* quotes */ false);
            }
        }
 
        /// <summary>
        /// Write the short value
        /// </summary>
        /// <param name="value">short value to be written</param>
        public void WriteValue(short value)
        {
            this.WriteCore(value.ToString(CultureInfo.InvariantCulture), /* quotes */ false);
        }
 
        /// <summary>
        /// Write the long value
        /// </summary>
        /// <param name="value">long value to be written</param>
        public void WriteValue(long value)
        {
            // Since Json only supports number, we need to convert long into string to prevent data loss
            this.WriteCore(value.ToString(CultureInfo.InvariantCulture), /* quotes */ true);
        }
 
        /// <summary>
        /// Write the double value
        /// </summary>
        /// <param name="value">double value to be written</param>
        public void WriteValue(double value)
        {
            if (double.IsInfinity(value) || double.IsNaN(value))
            {
                this.WriteCore(value.ToString(null, CultureInfo.InvariantCulture), true /*quotes*/);
            }
            else
            {
                // double.ToString() supports a max scale of 14,
                // whereas float.MinValue and float.MaxValue have 16 digits scale. Hence we need
                // to use XmlConvert in all other cases, except infinity
                this.WriteCore(XmlConvert.ToString(value), /* quotes */ false);
            }
        }
 
        /// <summary>
        /// Write the Guid value
        /// </summary>
        /// <param name="value">double value to be written</param>
        public void WriteValue(Guid value)
        {
            this.WriteCore(value.ToString(), /* quotes */ true);
        }
 
        /// <summary>
        /// Write the decimal value
        /// </summary>
        /// <param name="value">decimal value to be written</param>
        public void WriteValue(decimal value)
        {
            // Since Json doesn't have decimal support (it only has one data type - number),
            // we need to convert decimal to string to prevent data loss
            this.WriteCore(value.ToString(CultureInfo.InvariantCulture), /* quotes */ true);
        }
 
        /// <summary>
        /// Write the DateTime value
        /// </summary>
        /// <param name="dateTime">dateTime value to be written</param>
        public void WriteValue(DateTime dateTime)
        {
            // taken from the Atlas serializer
            // DevDiv 41127: Never confuse atlas serialized strings with dates
            // Serialized date: "\/Date(123)\/"
            // sb.Append(@"""\/Date(");
            // sb.Append((datetime.ToUniversalTime().Ticks - DatetimeMinTimeTicks) / 10000);
            // sb.Append(@")\/""");
            switch (dateTime.Kind)
            {
                case DateTimeKind.Local:
                    dateTime = dateTime.ToUniversalTime();
                    break;
                case DateTimeKind.Unspecified:
                    dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Utc);
                    break;
                case DateTimeKind.Utc:
                    break;
            }
 
            System.Diagnostics.Debug.Assert(dateTime.Kind == DateTimeKind.Utc, "dateTime.Kind == DateTimeKind.Utc");
            this.WriteCore(
                String.Format(
                    CultureInfo.InvariantCulture,
                    JsonWriter.JsonDateTimeFormat, 
                    ((dateTime.Ticks - DatetimeMinTimeTicks) / 10000)),
                true);
        }
 
        /// <summary>
        /// Write the byte value
        /// </summary>
        /// <param name="value">byte value to be written</param>
        public void WriteValue(byte value)
        {
            this.WriteCore(value.ToString(CultureInfo.InvariantCulture), /* quotes */ false);
        }
 
        /// <summary>
        /// Write the sbyte value
        /// </summary>
        /// <param name="value">sbyte value to be written</param>
        public void WriteValue(sbyte value)
        {
            this.WriteCore(value.ToString(CultureInfo.InvariantCulture), /* quotes */ false);
        }
 
        /// <summary>
        /// Write the string value
        /// </summary>
        /// <param name="s">string value to be written</param>
        public void WriteValue(string s)
        {
            if (s == null)
            {
                this.WriteCore("null", /* quotes */ false);
            }
            else
            {
                this.WriteCore(QuoteJScriptString(s), /* quotes */ true);
            }
        }
 
        /// <summary>
        /// Clears all buffers for the current writer
        /// </summary>
        public void Flush()
        {
            this.writer.Flush();
        }
 
        /// <summary>
        /// Returns the string value with special characters escaped
        /// </summary>
        /// <param name="s">input string value</param>
        /// <returns>Returns the string value with special characters escaped.</returns>
        private static string QuoteJScriptString(string s)
        {
            if (String.IsNullOrEmpty(s))
            {
                return String.Empty;
            }
 
            StringBuilder b = null;
            int startIndex = 0;
            int count = 0;
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
 
                // Append the unhandled characters (that do not require special treament)
                // to the string builder when special characters are detected.
                if (c == '\r' || c == '\t' || c == '\"' ||
                    c == '\\' || c == '\n' || c < ' ' || c > 0x7F || c == '\b' || c == '\f')
                {
                    // Flush out the unescaped characters we've built so far.
                    if (b == null)
                    {
                        b = new StringBuilder(s.Length + 6);
                    }
 
                    if (count > 0)
                    {
                        b.Append(s, startIndex, count);
                    }
 
                    startIndex = i + 1;
                    count = 0;
                }
 
                switch (c)
                {
                    case '\r':
                        b.Append("\\r");
                        break;
                    case '\t':
                        b.Append("\\t");
                        break;
                    case '\"':
                        b.Append("\\\"");
                        break;
                    case '\\':
                        b.Append("\\\\");
                        break;
                    case '\n':
                        b.Append("\\n");
                        break;
                    case '\b':
                        b.Append("\\b");
                        break;
                    case '\f':
                        b.Append("\\f");
                        break;
                    default:
                        if ((c < ' ') || (c > 0x7F))
                        {
                            b.AppendFormat(CultureInfo.InvariantCulture, "\\u{0:x4}", (int)c);
                        }
                        else
                        {
                            count++;
                        }
 
                        break;
                }
            }
 
            string processedString = s;
            if (b != null)
            {
                if (count > 0)
                {
                    b.Append(s, startIndex, count);
                }
 
                processedString = b.ToString();
            }
 
            return processedString;
        }
 
        /// <summary>
        /// Write the string value with/without quotes
        /// </summary>
        /// <param name="text">string value to be written</param>
        /// <param name="quotes">put quotes around the value if this value is true</param>
        private void WriteCore(string text, bool quotes)
        {
            if (this.scopes.Count != 0)
            {
                Scope currentScope = this.scopes.Peek();
                if (currentScope.Type == ScopeType.Array)
                {
                    if (currentScope.ObjectCount != 0)
                    {
                        this.writer.WriteTrimmed(", ");
                    }
 
                    currentScope.ObjectCount++;
                }
            }
 
            if (quotes)
            {
                this.writer.Write('"');
            }
 
            this.writer.Write(text);
            if (quotes)
            {
                this.writer.Write('"');
            }
        }
 
        /// <summary>
        /// Start the scope given the scope type
        /// </summary>
        /// <param name="type">scope type</param>
        private void StartScope(ScopeType type)
        {
            if (this.scopes.Count != 0)
            {
                Scope currentScope = this.scopes.Peek();
                if ((currentScope.Type == ScopeType.Array) &&
                    (currentScope.ObjectCount != 0))
                {
                    this.writer.WriteTrimmed(", ");
                }
 
                currentScope.ObjectCount++;
            }
 
            Scope scope = new Scope(type);
            this.scopes.Push(scope);
 
            if (type == ScopeType.Array)
            {
                this.writer.Write("[");
            }
            else
            {
                this.writer.Write("{");
            }
 
            this.writer.Indent++;
            this.writer.WriteLine();
        }
 
        /// <summary>
        /// class representing scope information
        /// </summary>
        private sealed class Scope
        {
            /// <summary> keeps the count of the nested scopes </summary>
            private int objectCount;
 
            /// <summary> keeps the type of the scope </summary>
            private ScopeType type;
 
            /// <summary>
            /// Creates a new instance of scope type
            /// </summary>
            /// <param name="type">type of the scope</param>
            public Scope(ScopeType type)
            {
                this.type = type;
            }
 
            /// <summary>
            /// Get/Set the object count for this scope
            /// </summary>
            public int ObjectCount
            {
                get
                {
                    return this.objectCount;
                }
 
                set
                {
                    this.objectCount = value;
                }
            }
 
            /// <summary>
            /// Gets the scope type for this scope
            /// </summary>
            public ScopeType Type
            {
                get
                {
                    return this.type;
                }
            }
        }
    }
}