Skip to main content

Represents a writer that provides a fast, non-cached, forward-only way of generating streams or files containing JSON Text according to the grammar rules laid out in https://www.ietf.org/rfc/rfc4627.txt.

#region License, Terms and Author(s)
//
// ELMAH - Error Logging Modules and Handlers for ASP.NET
// Copyright (c) 2004-9 Atif Aziz. All rights reserved.
//
//  Author(s):
//
//      Atif Aziz, http://www.raboof.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

[assembly: Elmah.Scc("$Id: JsonTextWriter.cs 640 2009-06-01 17:22:02Z azizatif $")]

namespace Elmah
{
    #region Imports

    using System;
    using System.Globalization;
    using System.IO;
    using System.Xml;

    #endregion

    /// <summary>
    /// Represents a writer that provides a fast, non-cached, forward-only
    /// way of generating streams or files containing JSON Text according
    /// to the grammar rules laid out in
    /// <a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>.
    /// </summary>
    /// <remarks>
    /// This class supports ELMAH and is not intended to be used directly
    /// from your code. It may be modified or removed in the future without
    /// notice. It has public accessibility for testing purposes. If you
    /// need a general-purpose JSON Text encoder, consult
    /// <a href="http://www.json.org/">JSON.org</a> for implementations
    /// or use classes available from the Microsoft .NET Framework.
    /// </remarks>
    public sealed class JsonTextWriter
    {
        private readonly TextWriter _writer;
        private readonly int[] _counters;
        private readonly char[] _terminators;
        private string _memberName;

        public JsonTextWriter(TextWriter writer)
        {
            Debug.Assert(writer != null);
            _writer = writer;
            const int levels = 10 + /* root */ 1;
            _counters = new int[levels];
            _terminators = new char[levels];
        }

        public int Depth { get; private set; }

        private int ItemCount
        {
            get { return _counters[Depth]; }
            set { _counters[Depth] = value; }
        }

        private char Terminator
        {
            get { return _terminators[Depth]; }
            set { _terminators[Depth] = value; }
        }

        public JsonTextWriter Object()
        {
            return StartStructured("{", "}");
        }

        public JsonTextWriter EndObject()
        {
            return Pop();
        }

        public JsonTextWriter Array()
        {
            return StartStructured("[", "]");
        }

        public JsonTextWriter EndArray()
        {
            return Pop();
        }

        public JsonTextWriter Pop()
        {
            return EndStructured();
        }

        public JsonTextWriter Member(string name)
        {
            if (name == null) throw new ArgumentNullException("name");
            if (_memberName != null) throw new InvalidOperationException("Missing member value.");
            _memberName = name;
            return this;
        }

        private JsonTextWriter Write(string text)
        {
            return WriteImpl(text, /* raw */ false);
        }

        private JsonTextWriter WriteEnquoted(string text)
        {
            return WriteImpl(text, /* raw */ true);
        }

        private JsonTextWriter WriteImpl(string text, bool raw)
        {
            Debug.Assert(raw || !string.IsNullOrEmpty(text));

            if (Depth == 0 && (text.Length > 1 || (text[0] != '{' && text[0] != '[')))
                throw new InvalidOperationException();

            var writer = _writer;

            if (ItemCount > 0)
                writer.Write(',');

            var name = _memberName;
            _memberName = null;

            if (name != null)
            {
                writer.Write(' ');
                Enquote(name, writer);
                writer.Write(':');
            }

            if (Depth > 0)
                writer.Write(' ');

            if (raw)
                Enquote(text, writer);
            else
                writer.Write(text);

            ItemCount = ItemCount + 1;

            return this;
        }

        public JsonTextWriter Number(int value)
        {
            return Write(value.ToString(CultureInfo.InvariantCulture));
        }

        public JsonTextWriter String(string str)
        {
            return str == null ? Null() : WriteEnquoted(str);
        }

        public JsonTextWriter Null()
        {
            return Write("null");
        }

        public JsonTextWriter Boolean(bool value)
        {
            return Write(value ? "true" : "false");
        }

        private static readonly DateTime Epoch =  new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        public JsonTextWriter Number(DateTime time)
        {
            var seconds = time.ToUniversalTime().Subtract(Epoch).TotalSeconds;
            return Write(seconds.ToString(CultureInfo.InvariantCulture));
        }

        public JsonTextWriter String(DateTime time)
        {
            return String(XmlConvert.ToString(time, XmlDateTimeSerializationMode.Utc));
        }

        private JsonTextWriter StartStructured(string start, string end)
        {
            if (Depth + 1 == _counters.Length)
                throw new Exception();

            Write(start);
            Depth++;
            Terminator = end[0];
            return this;
        }

        private JsonTextWriter EndStructured()
        {
            if (Depth - 1 < 0)
                throw new Exception();

            _writer.Write(' ');
            _writer.Write(Terminator);
            ItemCount = 0;
            Depth--;
            return this;
        }

        private static void Enquote(string s, TextWriter writer)
        {
            Debug.Assert(writer != null);

            var length = (s ?? string.Empty).Length;

            writer.Write('"');

            var ch = '\0';

            for (var index = 0; index < length; index++)
            {
                var last = ch;

                Debug.Assert(s != null);
                ch = s[index];

                switch (ch)
                {
                    case '\\':
                    case '"':
                    {
                        writer.Write('\\');
                        writer.Write(ch);
                        break;
                    }

                    case '/':
                    {
                        if (last == '<')
                            writer.Write('\\');
                        writer.Write(ch);
                        break;
                    }

                    case '\b': writer.Write("\\b"); break;
                    case '\t': writer.Write("\\t"); break;
                    case '\n': writer.Write("\\n"); break;
                    case '\f': writer.Write("\\f"); break;
                    case '\r': writer.Write("\\r"); break;

                    default:
                    {
                        if (ch < ' ')
                        {
                            writer.Write("\\u");
                            writer.Write(((int)ch).ToString("x4", CultureInfo.InvariantCulture));
                        }
                        else
                        {
                            writer.Write(ch);
                        }

                        break;
                    }
                }
            }

            writer.Write('"');
        }
    }
}