Manatee.Json / Json / JsonObject.cs

/***************************************************************************************

	Copyright 2012 Greg Dennis

	   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.
 
	File Name:		JsonObject.cs
	Namespace:		Manatee.Json
	Class Name:		JsonObject
	Purpose:		Represents a collection of key:value pairs in a JSON
					structure.

***************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using Manatee.Json.Enumerations;
using Manatee.Json.Exceptions;
using Manatee.Json.Helpers;
using Manatee.StateMachine;
using Manatee.StateMachine.Exceptions;

namespace Manatee.Json
{
	/// <summary>
	/// Represents a collection of key:value pairs in a JSON structure.
	/// </summary>
	/// <remarks>
	/// A key is always represented as a string.  A value can consist of a string, a numerical value, 
	/// a boolean (true or false), a null placeholder, a JSON array of values, or a nested JSON object.
	/// </remarks>
	public class JsonObject : Dictionary<string, JsonValue>
	{
		enum State
		{
			Start,
			Key,
			Colon,
			Value,
			End
		}

		private static readonly StateMachine<State, JsonInput> StateMachine = new StateMachine<State, JsonInput>();

		private readonly string _source;
		private string _key;
		private int _index;
		private JsonValue _value;
		private bool _done;
		private readonly InputStream<JsonInput> _stream = new InputStream<JsonInput>();

		/// <summary>
		/// Gets or sets the value associated with the specified key.
		/// </summary>
		/// <param name="key">The key of the value to get or set.</param>
		/// <returns>The value associated with the specified key.</returns>
		public new JsonValue this[string key]
		{
			get { return base[key]; }
			set { base[key] = value ?? JsonValue.Null; }
		}

		static JsonObject()
		{
			StateMachine[State.Start, JsonInput.OpenBrace] = GotStart;
			StateMachine[State.Key, JsonInput.Quote] = GotKey;
			StateMachine[State.Key, JsonInput.CloseBrace] = GotEmpty;
			StateMachine[State.Colon, JsonInput.Colon] = GotColon;
			StateMachine[State.Value, JsonInput.OpenBrace] = GotValue;
			StateMachine[State.Value, JsonInput.Quote] = GotValue;
			StateMachine[State.Value, JsonInput.Number] = GotValue;
			StateMachine[State.Value, JsonInput.Boolean] = GotValue;
			StateMachine[State.Value, JsonInput.Null] = GotValue;
			StateMachine[State.Value, JsonInput.OpenBracket] = GotValue;
			StateMachine[State.End, JsonInput.Comma] = GotEnd;
			StateMachine[State.End, JsonInput.CloseBrace] = GotEnd;
			StateMachine.UpdateFunction = GetNextInput;
		}
		/// <summary>
		/// Creates an empty instance of a JSON object.
		/// </summary>
		public JsonObject() {}
		/// <summary>
		/// Creates an instance of a JSON object and fills it by parsing the
		/// supplied string.
		/// </summary>
		/// <param name="s">A string.</param>
		public JsonObject(string s)
			: this()
		{
			_source = StripExternalSpaces(s);
			Parse(0);
		}
		internal JsonObject(string s, ref int i)
			: this()
		{
			_source = s;
			i = Parse(i);
		}
		/// <summary>
		/// Finalizes memory management responsibilities.
		/// </summary>
		~JsonObject()
		{
			StateMachine.UnregisterOwner(this);
		}

		/// <summary>
		/// Creates a string representation of the JSON data.
		/// </summary>
		/// <param name="indentLevel">The indention level for the object.</param>
		/// <returns>A string.</returns>
		public string GetIndentedString(int indentLevel = 0)
		{
			if (Count == 0) return string.Empty;
			string key, tab0 = string.Empty.PadLeft(indentLevel, '\t'),
				   tab1 = string.Empty.PadLeft(indentLevel + 1, '\t'),
				   tab2 = string.Empty.PadLeft(indentLevel + 2, '\t'),
				   s = "{\n";
			int i;
			for (i = 0; i < Count - 1; i++)
			{
				key = Keys.ElementAt(i);
				if (this[key] == null) this[key] = new JsonValue();
				s += string.Format("{0}\"{1}\" :\n{2}{3},\n", tab1, key, tab2, this[key].GetIndentedString(indentLevel + 2));
			}
			key = Keys.ElementAt(i);
			s += string.Format("{0}\"{1}\" :\n{2}{3}\n{4}}}", tab1, key, tab2, this[key].GetIndentedString(indentLevel + 2), tab0);
			return s;
		}
		/// <summary>
		/// Adds the specified key and value to the dictionary.
		/// </summary>
		/// <param name="key">The key of the element to add.</param>
		/// <param name="value">The value of the element to add. The value can be null for reference types.</param>
		public new void Add(string key, JsonValue value)
		{
			base.Add(key, value ?? JsonValue.Null);
		}

		private static bool IsWhiteSpace(char c)
		{
			return (c == 10) || (c == 13) || (c == 32) || (c == 9);
		}
		private static string StripExternalSpaces(string s)
		{
			var inString = false;
			var ret = "";
			foreach (var t in s)
			{
				if (t == '"') inString = !inString;
				if (inString || !IsWhiteSpace(t))
					ret += t;
			}
			return ret;
		}
		private int Parse(int i)
		{
			_stream.Clear();
			_value = null;
			_index = i;
			_done = false;
			try
			{
				StateMachine.Run(this, State.Start, _stream);
				if (!_done)
					throw new JsonSyntaxException(_index);
			}
			catch (InputNotValidForStateException<State, JsonInput>)
			{
				throw new JsonSyntaxException(_index);
			}
			catch (StateNotValidException<State>)
			{
				throw new JsonSyntaxException(_index);
			}
			catch (ActionNotDefinedForStateAndInputException<State, JsonInput>)
			{
				throw new JsonSyntaxException(_index);
			}
			catch (Exception e)
			{
				e.Data.Add("source", _source);
				e.Data.Add("index", _index);
				throw;
			}
			return _index;
		}

		private static string GetKey(string source, ref int index)
		{
			var temp = source.Substring(index);
			var length = temp.IndexOf('"');
			if (length < 0)
				throw new JsonKeyParseException(index);
			if (length == 0)
			{
				index += 1;
				return string.Empty;
			}
			index += length + 1;
			return temp.Substring(0, length);
		}

		/// <summary>
		/// Creates a string representation of the JSON data.
		/// </summary>
		/// <returns>A string.</returns>
		/// <remarks>
		/// Passing the returned string back into the parser will result in a copy of
		/// this JSON object.
		/// </remarks>
		public override string ToString()
		{
			if (Count == 0) return "{}";
			return "{" + string.Join(",", from kvp in this
										  select string.Format("\"{0}\":{1}", kvp.Key, kvp.Value)) + "}";
		}
		/// <summary>
		/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
		/// </summary>
		/// <returns>
		/// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
		/// </returns>
		/// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>. </param><filterpriority>2</filterpriority>
		public override bool Equals(object obj)
		{
			var json = obj as JsonObject;
			if (json == null) return false;
			return Keys.All(json.ContainsKey) && (Keys.Count == json.Keys.Count) &&
			       this.All(pair => json[pair.Key].Equals(pair.Value));
		}
		/// <summary>
		/// Serves as a hash function for a particular type. 
		/// </summary>
		/// <returns>
		/// A hash code for the current <see cref="T:System.Object"/>.
		/// </returns>
		/// <filterpriority>2</filterpriority>
		public override int GetHashCode()
		{
			return base.GetHashCode();
		}

		private static void GetNextInput(object owner)
		{
			var obj = owner as JsonObject;
			if (obj == null) return;
			if (obj._done || (obj._index == obj._source.Length)) return;
			try
			{
				var next = CharacterConverter.Item(obj._source[obj._index++]);
				obj._stream.Add(next);
			}
			catch (KeyNotFoundException)
			{
				throw new JsonSyntaxException(obj._index);
			}
		}
		private static State GotStart(object owner, JsonInput input)
		{
			return State.Key;
		}
		private static State GotKey(object owner, JsonInput input)
		{
			var obj = owner as JsonObject;
			obj._key = GetKey(obj._source, ref obj._index);
			return State.Colon;
		}
		private static State GotColon(object owner, JsonInput input)
		{
			return State.Value;
		}
		private static State GotValue(object owner, JsonInput input)
		{
			var obj = owner as JsonObject;
			obj._value = JsonValue.Parse(obj._source, ref obj._index);
			return State.End;
		}
		private static State GotEmpty(object owner, JsonInput input)
		{
			var obj = owner as JsonObject;
			obj._done = (input == JsonInput.CloseBrace);
			if (obj.Count != 0)
				throw new JsonSyntaxException(obj._index);
			return State.Value;
		}
		private static State GotEnd(object owner, JsonInput input)
		{
			var obj = owner as JsonObject;
			obj[obj._key] = obj._value;
			obj._done = (input == JsonInput.CloseBrace);
			return State.Key;
		}

	}
}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.