File: System\Data\Services\Serializers\JsonReader.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="JsonReader.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides a reader implementaion for Json format
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Serializers
{
    #region Namespaces.
 
    using System;
    using System.Collections;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Text;
    using System.Text.RegularExpressions;
using System.Collections.Generic;
 
    #endregion Namespaces.
 
    /// <summary>Json text reader.</summary>
    /// <remarks>Does not dispose the reader, since we don't own the underlying stream.</remarks>
    internal sealed class JsonReader
    {
        /// <summary>Compiled Regex for DateTime Format.</summary>
        private static readonly Regex DateTimeFormat = new Regex(@"^/Date\((?<ticks>-?[0-9]+)\)/", RegexOptions.Compiled);
 
        /// <summary>Maximum recursion limit on reader.</summary>
        private const int RecursionLimit = 200;
 
        /// <summary> Reader to reader text into </summary>
        private readonly StreamReader reader;
 
        /// <summary>Depth of recursion.</summary>
        private int recursionDepth;
 
        /// <summary>
        /// Creates a new instance of Json reader which readers the json text 
        /// from the given reader
        /// </summary>
        /// <param name="reader">text reader from which json payload needs to be read.
        /// Does not dispose the reader, since we don't own the underlying stream.</param>
        public JsonReader(StreamReader reader)
        {
            Debug.Assert(reader != null, "reader != null");
            this.reader = reader;
        }
 
        /// <summary>
        /// Converts the given value into the right type
        /// </summary>
        /// <returns>returns the clr object instance which </returns>
        public object ReadValue()
        {
            this.RecurseEnter();
 
            object value = null;
            bool allowNull = false;
 
            char ch = this.PeekNextSignificantCharacter();
            if (ch == '[')
            {
                value = this.ReadArray();
            }
            else if (ch == '{')
            {
                value = this.ReadObject();
            }
            else if ((ch == '\'') || (ch == '"'))
            {
                bool hasLeadingSlash;
                string s = this.ReadString(out hasLeadingSlash);
                value = s; // may be overwritten with a DateTime if ends up being a date/time
 
                // Atlas format for date/time
                if (hasLeadingSlash)
                {
                    Match match = DateTimeFormat.Match(s);
                    if (match.Success)
                    {
                        string ticksStr = match.Groups["ticks"].Value;
 
                        long ticks;
                        if (long.TryParse(ticksStr, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out ticks))
                        {
                            // The javascript ticks start from 1/1/1970 but FX DateTime ticks start from 1/1/0001
                            DateTime dateTime = new DateTime(ticks * 10000 + JsonWriter.DatetimeMinTimeTicks, DateTimeKind.Utc);
                            value = dateTime;
                        }
                    }
                }
            }
            else if (Char.IsDigit(ch) || (ch == '-') || (ch == '.'))
            {
                value = this.ReadNumber();
            }
            else if ((ch == 't') || (ch == 'f'))
            {
                value = this.ReadBoolean();
            }
            else if (ch == 'n')
            {
                this.ReadNull();
                allowNull = true;
            }
 
            if ((value == null) && (allowNull == false))
            {
                throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidContent);
            }
 
            this.RecurseLeave();
 
            // if there is junk data at the end of the stream ex. {...}junk
            // then an exception will be thrown.
            if (this.recursionDepth == 0)
            {
                // at the end of the stream
                if (this.PeekNextSignificantCharacter() != '\0')
                {
                    throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidContent);
                }
            }
 
            return value;
        }
 
        /// <summary>
        /// Gets the next character from the reader. This function moves the enumerator by 1 position
        /// </summary>
        /// <returns>the next charater from the current reader position</returns>
        private char ReadNextCharacter()
        {
            int result = this.reader.Read();
            if (result < 0)
            {
                throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidContent);
            }
 
            Debug.Assert(result <= char.MaxValue, "result <= char.MaxValue");
            return (char)result;
        }
 
        /// <summary>
        /// peeks the next character from the reader. This does not move the current reader position.
        /// </summary>
        /// <returns>the next character from the current reader position</returns>
        private char PeekNextCharacter()
        {
            if (this.reader.EndOfStream)
            {
                return '\0';
            }
 
            int result = this.reader.Peek();
            Debug.Assert(result >= 0, "Peek must not return value < 0 since we are not at EndOfStream yet.");
            Debug.Assert(result <= char.MaxValue, "result <= char.MaxValue");
            return (char)result;
        }
 
        /// <summary>
        /// Returns the next count characters from the reader's current position
        /// </summary>
        /// <param name="count">number of characters to return</param>
        /// <returns>string consisting of next count characters</returns>
        private string GetCharacters(int count)
        {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < count; i++)
            {
                char ch = this.ReadNextCharacter();
                stringBuilder.Append(ch);
            }
 
            return stringBuilder.ToString();
        }
 
        /// <summary>
        /// Sets the readers position to the next significant character position
        /// </summary>
        /// <returns>returns the next significant character without changing the current position of the reader</returns>
        private char PeekNextSignificantCharacter()
        {
            char ch = this.PeekNextCharacter();
            while ((ch != '\0') && Char.IsWhiteSpace(ch))
            {
                this.ReadNextCharacter();
                ch = this.PeekNextCharacter();
            }
 
            return ch;
        }
 
        /// <summary>
        /// Converts the given text into an arrayList
        /// </summary>
        /// <returns>returns the arraylist containing the list of objects</returns>
        private ArrayList ReadArray()
        {
            ArrayList array = new ArrayList();
 
            // Consume the '['
            this.ReadNextCharacter();
 
            while (true)
            {
                char ch = this.PeekNextSignificantCharacter();
                if (ch == '\0')
                {
                    throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidContent);
                }
 
                if (ch == ']')
                {
                    this.ReadNextCharacter();
                    return array;
                }
 
                if (array.Count != 0)
                {
                    if (ch != ',')
                    {
                        throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_MissingArrayMemberSeperator);
                    }
                    else
                    {
                        this.ReadNextCharacter();
                    }
                }
 
                object item = this.ReadValue();
                array.Add(item);
            }
        }
 
        /// <summary>
        /// Reads the boolean value
        /// </summary>
        /// <returns>returns the boolean value as read from the reader</returns>
        private bool ReadBoolean()
        {
            string s = this.ReadName(/* allowQuotes */ false);
 
            if (s != null)
            {
                if (s.Equals(XmlConstants.XmlTrueLiteral, StringComparison.Ordinal))
                {
                    return true;
                }
                else if (s.Equals(XmlConstants.XmlFalseLiteral, StringComparison.Ordinal))
                {
                    return false;
                }
            }
 
            throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidKeyword(s));
        }
 
        /// <summary>
        /// Reads the name string from the reader
        /// </summary>
        /// <param name="allowQuotes">true, if you want to allow quotes, otherwise false</param>
        /// <returns>string name value</returns>
        private string ReadName(bool allowQuotes)
        {
            char ch = this.PeekNextSignificantCharacter();
 
            if ((ch == '"') || (ch == '\''))
            {
                if (allowQuotes)
                {
                    return this.ReadString();
                }
            }
            else
            {
                StringBuilder sb = new StringBuilder();
 
                while (true)
                {
                    ch = this.PeekNextCharacter();
                    if ((ch == '_') || Char.IsLetterOrDigit(ch) || ch == '$')
                    {
                        this.ReadNextCharacter();
                        sb.Append(ch);
                    }
                    else
                    {
                        return sb.ToString();
                    }
                }
            }
 
            return null;
        }
 
        /// <summary>
        /// Reads the null literal from the reader
        /// </summary>
        private void ReadNull()
        {
            string s = this.ReadName(/* allowQuotes */ false);
 
            if (s == null)
            {
                throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_MissingMemberName);
            }
            else if (!s.Equals("null", StringComparison.Ordinal))
            {
                throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidKeyword(s));
            }
        }
 
        /// <summary>Reads the number from the reader.</summary>
        /// <returns>reads the clr number object</returns>
        private object ReadNumber()
        {
            char ch = this.ReadNextCharacter();
 
            StringBuilder sb = new StringBuilder();
            sb.Append(ch);
            while (true)
            {
                ch = this.PeekNextSignificantCharacter();
 
                if (Char.IsDigit(ch) || (ch == '.') || (ch == 'E') || (ch == 'e') || (ch == '-') || (ch == '+'))
                {
                    this.ReadNextCharacter();
                    sb.Append(ch);
                }
                else
                {
                    break;
                }
            }
 
            string s = sb.ToString();
            Double doubleValue;
            int intValue;
 
            // We will first try and convert this int32. If this succeeds, great. Otherwise, we will try
            // and convert this into a double.
            if (Int32.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out intValue))
            {
                return intValue;
            }
            else if (Double.TryParse(s, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out doubleValue))
            {
                return doubleValue;
            }
 
            throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidContent);
        }
 
        /// <summary>
        /// Reads the object from the reader
        /// </summary>
        /// <returns>returns hashtable containing the list of property names and values</returns>
        private JsonObjectRecords ReadObject()
        {
            JsonObjectRecords record = new JsonObjectRecords();            
 
            // Consume the '{'
            this.ReadNextCharacter();
 
            while (true)
            {
                char ch = this.PeekNextSignificantCharacter();
                if (ch == '\0')
                {
                    // Unterminated Object literal
                    throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidContent);
                }
 
                if (ch == '}')
                {
                    this.ReadNextCharacter();
                    return record;
                }
 
                if (record.Count != 0)
                {
                    if (ch != ',')
                    {
                        throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_MissingMemberSeperator);
                    }
                    else
                    {
                        this.ReadNextCharacter();
                    }
                }
 
                string name = this.ReadName(/* allowQuotes */ true);
                if (String.IsNullOrEmpty(name))
                {
                    throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidJsonNameSpecifiedOrExtraComma);
                }
 
                ch = this.PeekNextSignificantCharacter();
 
                // Unexpected name/value pair syntax in object literal
                if (ch != ':')
                {
                    throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_MissingNameValueSeperator(name));
                }
                else
                {
                    this.ReadNextCharacter();
                }
 
                object item = this.ReadValue();
                
                record.Add(name, item);
            }
        }
 
        /// <summary>
        /// Read the string value from the reader
        /// </summary>
        /// <returns>returns the string value read from the reader</returns>
        private string ReadString()
        {
            bool dummy;
            return this.ReadString(out dummy);
        }
 
        /// <summary>
        /// Read the string value from the reader
        /// </summary>
        /// <param name="hasLeadingSlash">out parameter indicating whether the string has a leading slash or not</param>
        /// <returns>returns the string value read from the reader</returns>
        private string ReadString(out bool hasLeadingSlash)
        {
            char endQuoteCharacter = this.ReadNextCharacter();
            char ch = this.ReadNextCharacter();
 
            hasLeadingSlash = (ch == '\\') ? true : false;
            StringBuilder sb = new StringBuilder();
 
            while (true)
            {
                if (ch == '\\')
                {
                    ch = this.ReadNextCharacter();
                    
                    // From 4627, section 2.5: Strings, here's the list of characters that we should be escaping
                    if (ch == 'u')
                    {
                        string unicodeSequence = this.GetCharacters(4);
                        Debug.Assert(unicodeSequence != null, "unicodeSequence != null");
                        ch = (char)Int32.Parse(unicodeSequence, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                        sb.Append(ch);
                    }
                    else if (ch == 'b')
                    {
                        sb.Append('\b');
                    }
                    else if (ch == 'f')
                    {
                        sb.Append('\f');
                    }
                    else if (ch == 'n')
                    {
                        sb.Append('\n');
                    }
                    else if (ch == 'r')
                    {
                        sb.Append('\r');
                    }
                    else if (ch == 't')
                    {
                        sb.Append('\t');
                    }
                    else if (ch == '\\' || ch == '\"' || ch == '/' || ch == '\'')
                    {
                        sb.Append(ch);
                    }
                    else
                    {
                        throw DataServiceException.CreateBadRequestError(Strings.BadRequestStream_InvalidJsonUnrecognizedEscapeSequence);
                    }
                }
                else
                if (ch == endQuoteCharacter)
                {
                    return sb.ToString();
                }
                else
                {
                    sb.Append(ch);
                }
            
                ch = this.ReadNextCharacter();
            }
        }
 
        /// <summary>Marks the fact that a recursive method was entered, and checks that the depth is allowed.</summary>
        private void RecurseEnter()
        {
            WebUtil.RecurseEnter(RecursionLimit, ref this.recursionDepth);
        }
 
        /// <summary>Marks the fact that a recursive method is leaving..</summary>
        private void RecurseLeave()
        {
            WebUtil.RecurseLeave(ref this.recursionDepth);
        }
 
        /// <summary>
        /// Represent one Json Object. 
        /// </summary>
        /// <remarks>This class honors the original ordering of the inner elements of the json object.</remarks>
        public class JsonObjectRecords
        {
            /// <summary>
            /// A list of keys in the object
            /// </summary>
            private List<String> orderedKeys;
 
            /// <summary>
            /// The actual storage of key-value pair
            /// </summary>
            private Dictionary<String, Object> entries;
            
            /// <summary>
            /// Constructor
            /// </summary>
            public JsonObjectRecords()
            {
                this.orderedKeys = new List<string>();
                this.entries = new Dictionary<string, object>(EqualityComparer<String>.Default);
            }
 
            /// <summary>
            /// Number of elements in this object
            /// </summary>
            public int Count
            {
                get { return this.orderedKeys.Count; }
            }
 
            /// <summary>
            /// A list of keys in the object, in the order which they are deserialized
            /// </summary>
            public List<String> OrderedKeys
            {
                get { return this.orderedKeys; }
            }
 
            /// <summary>
            /// The actual storage of key-value pair
            /// </summary>
            public Dictionary<String, Object> Entries
            {
                get { return this.entries; }
            }
 
            /// <summary>
            /// Add a new element into the object
            /// </summary>
            /// <param name="key">Key</param>
            /// <param name="value">Value</param>
            public void Add(String key, Object value)
            {
                // Json Object used to be a hashtable
                // It will automatically resolve duplicated keys
                // however, for that scenario, we must not update the order table
                if (!this.entries.ContainsKey(key))
                {
                    this.orderedKeys.Add(key);
                }
 
                this.entries[key] = value;
            }
        }
    }
}