note

	description: "Parse serialized JSON data"
	author: "jvelilla"
	date: "2008/08/24"
	revision: "Revision 0.1"

class
	JSON_PARSER

inherit
	JSON_READER
	JSON_TOKENS

create
	make_parser

feature {NONE} -- Initialize

	make_parser (a_json: STRING)
			-- Initialize.
		require
			json_not_empty: a_json /= Void and then not a_json.is_empty
		do
			make (a_json)
			is_parsed := True
			create errors.make
		end

feature -- Status report

	is_parsed: BOOLEAN
			-- Is parsed?

	errors: LINKED_LIST [STRING]
			-- Current errors

	current_errors: STRING
			-- Current errors as string
		do
			create Result.make_empty
			from
				errors.start
			until
				errors.after
			loop
				Result.append_string (errors.item + "%N")
				errors.forth
			end
		end

feature -- Element change

	report_error (e: STRING)
			-- Report error `e'
		require
			e_not_void: e /= Void
		do
			errors.force (e)
		end

feature -- Commands

	parse_json: detachable JSON_VALUE
		    -- Parse JSON data `representation'
		    -- start ::= object | array
		do
		   	if is_valid_start_symbol then
		   	    Result := parse
		        if extra_elements then
		            is_parsed := False
		        end
		    else
		        is_parsed := False
		        report_error ("Syntax error unexpected token, expecting `{' or `['")
		    end
		end

	parse: detachable JSON_VALUE
			-- Parse JSON data `representation'
		local
			c: CHARACTER
		do
			if is_parsed then
				skip_white_spaces
				c := actual
				inspect c
				when j_OBJECT_OPEN then
					Result := parse_object
				when j_STRING then
					Result := parse_string
				when j_ARRAY_OPEN then
					Result := parse_array
				else
					if c.is_digit or c = j_MINUS  then
						Result := parse_number
					elseif is_null then
						Result := create {JSON_NULL}
						next
						next
						next
					elseif is_true then
						Result := create {JSON_BOOLEAN}.make_boolean (True)
						next
						next
						next
					elseif is_false then
						Result := create {JSON_BOOLEAN}.make_boolean (False)
						next
						next
						next
						next
					else
						is_parsed := False
						report_error ("JSON is not well formed in parse")
						Result := Void
					end
				end
			end
		ensure
			is_parsed_implies_result_not_void: is_parsed implies Result /= Void
		end

	parse_object: JSON_OBJECT
			-- object
			-- {}
			-- {"key" : "value" [,]}
		local
			has_more: BOOLEAN
			l_json_string: detachable JSON_STRING
			l_value: detachable JSON_VALUE
		do
			create Result.make
			-- check if is an empty object {}
			next
			skip_white_spaces
			if actual = j_OBJECT_CLOSE then
				--is an empty object
			else
				-- a complex object {"key" : "value"}
				previous
				from has_more := True until not has_more loop
					next
					skip_white_spaces
					l_json_string := parse_string
					next
					skip_white_spaces
					if actual = ':' then
						next
						skip_white_spaces
					else
						is_parsed := False
						report_error ("%N Input string is a not well formed JSON, expected: : found: " + actual.out)
						has_more := False
					end

					l_value := parse
					if is_parsed and then (l_value /= Void and l_json_string /= Void) then
						Result.put (l_value, l_json_string)
						next
						skip_white_spaces
						if actual = j_OBJECT_CLOSE then
							has_more := False
						elseif actual /= ',' then
							has_more := False
							is_parsed := False
							report_error ("JSON Object syntactically malformed expected , found: [" + actual.out + "]")
						end
					else
						has_more := False
						-- explain the error
					end
				end
			end
		end

	parse_string: detachable JSON_STRING
			-- Parsed string
		local
			has_more: BOOLEAN
			l_json_string: STRING
			l_unicode: STRING
			c: like actual
		do
			create l_json_string.make_empty
			if actual = j_STRING then
				from
					has_more := True
				until
					not has_more
				loop
					next
					c := actual
					if c = j_STRING then
						has_more := False
					elseif c = '%H' then
						next
						c := actual
						if c = 'u' then
							create l_unicode.make_from_string ("\u")
							l_unicode.append (read_unicode)
							c := actual
							if is_valid_unicode (l_unicode) then
								l_json_string.append (l_unicode)
							else
								has_more := False
								is_parsed := False
								report_error ("Input String is not well formed JSON, expected a Unicode value, found [" + c.out + " ]")
							end
						elseif (not is_special_character (c) and not is_special_control (c)) or c = '%N' then
							has_more := False
							is_parsed := False
							report_error ("Input String is not well formed JSON, found [" + c.out + " ]")
						else
							l_json_string.append_character ('\')
							l_json_string.append_character (c)
						end
					else
						if is_special_character (c) and c /= '/' then
							has_more := False
							is_parsed := False
							report_error ("Input String is not well formed JSON, found [" + c.out + " ]")
						else
							l_json_string.append_character (c)
						end
					end
				end
				create Result.make_with_escaped_json (l_json_string)
			else
				Result := Void
			end
		end

	parse_array: JSON_ARRAY
			-- array
			-- []
			-- [elements [,]]
		local
			flag: BOOLEAN
			l_value: detachable JSON_VALUE
			c: like actual
		do
			create Result.make_array
			--check if is an empty array []
			next
			skip_white_spaces
			if actual = j_array_close then
				--is an empty array
			else
				previous
				from
					flag := True
				until
					not flag
				loop
					next
					skip_white_spaces
					l_value := parse
					if is_parsed and then l_value /= Void then
						Result.add (l_value)
						next
						skip_white_spaces
						c := actual
						if c = j_ARRAY_CLOSE then
							flag := False
 						elseif c /= ',' then
							flag := False
							is_parsed := False
							report_error ("Array is not well formed JSON,  found [" + c.out + " ]")
						end
					else
						flag := False
						report_error ("Array is not well formed JSON,  found [" + actual.out + " ]")
					end
				end
			end
		end

	parse_number: detachable JSON_NUMBER
			-- Parsed number
		local
			sb: STRING
			flag: BOOLEAN
			is_integer: BOOLEAN
			c: like actual
		do
			create sb.make_empty
			sb.append_character (actual)

			from
				flag := True
			until
				not flag
			loop
				next
				c := actual
				if not has_next or is_close_token (c)
					or c = ',' or c = '%N' or c = '%R'
				then
					flag := False
					previous
				else
					sb.append_character (c)
				end
			end

			if is_valid_number (sb) then
				if sb.is_integer then
					create Result.make_integer (sb.to_integer)
					is_integer := True
				elseif sb.is_double and not is_integer then
					create Result.make_real (sb.to_double)
				end
			else
				is_parsed := False
				report_error ("Expected a number, found: [ " + sb  + " ]")
			end
		end

	is_null: BOOLEAN
			-- Word at index represents null?
		local
			l_null: STRING
			l_string: STRING
		do
			l_null := null_id
			l_string := json_substring (index,index + l_null.count - 1)
			if l_string.is_equal (l_null) then
				Result := True
			end
		end

	is_false: BOOLEAN
			-- Word at index represents false?
		local
			l_false: STRING
			l_string: STRING
		do
			l_false := false_id
			l_string := json_substring (index, index + l_false.count - 1)
			if l_string.is_equal (l_false) then
				Result := True
			end
		end

	is_true: BOOLEAN
			-- Word at index represents true?
		local
			l_true: STRING
			l_string: STRING
		do
			l_true := true_id
			l_string := json_substring (index,index + l_true.count - 1)
			if l_string.is_equal (l_true) then
				Result := True
			end
		end

	read_unicode: STRING
			-- Read Unicode and return value
		local
			i: INTEGER
		do
			create Result.make_empty
			from
				i := 1
			until
				i > 4 or not has_next
			loop
				next
				Result.append_character (actual)
				i := i + 1
			end
		end

feature {NONE} -- Implementation

	is_valid_number (a_number: STRING): BOOLEAN
            -- is 'a_number' a valid number based on this regular expression
            -- "-?(?: 0|[1-9]\d+)(?: \.\d+)?(?: [eE][+-]?\d+)?\b"?
		local
			s: detachable STRING
			c: CHARACTER
			i,n: INTEGER
		do
			create s.make_empty
			n := a_number.count
			if n = 0 then
				Result := False
			else
				Result := True
				i := 1
				--| "-?"
				c := a_number[i]
				if c = '-' then
					s.extend (c); i := i + 1; c := a_number[i]
				end
				--| "0|[1-9]\d*
				if c.is_digit then
					if c = '0' then
						--| "0"
						s.extend (c); i := i + 1; c := a_number[i]
					else
						--| "[1-9]"
						s.extend (c); i := i + 1; c := a_number[i]
						--| "\d*"
						from until i > n or not c.is_digit loop
							s.extend (c); i := i + 1; c := a_number[i]
						end
					end
				end
			end
			if Result then
					--| "(\.\d+)?"			
				if c = '.' then
					--| "\.\d+"  =  "\.\d\d*"
					s.extend (c); i := i + 1; c := a_number[i]
					if c.is_digit then
						from until i > n or not c.is_digit loop
							s.extend (c); i := i + 1; c := a_number[i]
						end
					else
						Result := False --| expecting digit
					end
				end
			end
			if Result then --| "(?:[eE][+-]?\d+)?\b"
				if c = 'e' or c = 'E' then
					--| "[eE][+-]?\d+"
					s.extend (c); i := i + 1; c := a_number[i]
					if c = '+' or c = '-' then
 							s.extend (c); i := i + 1; c := a_number[i]
					end
					if c.is_digit then
						from until i > n or not c.is_digit loop
							s.extend (c); i := i + 1; c := a_number[i]
						end
					else
						Result := False --| expecting digit							
					end
				end
			end
			if Result then --| "\b"
				from until i > n or not c.is_space loop
					s.extend (c); i := i + 1; c := a_number[i]
				end
				Result := i > n and then s.same_string (a_number)
			end
		end

	is_valid_unicode (a_unicode: STRING): BOOLEAN
			-- is 'a_unicode' a valid Unicode based on this regular expression
			-- "\\u[0-9a-fA-F]{4}"
		local
			i: INTEGER
		do
			if
				a_unicode.count = 6 and then
				a_unicode[1] = '\' and then
				a_unicode[2] = 'u'
			then
				from
					Result := True
					i := 3
				until
					i > 6 or Result = False
				loop
					inspect a_unicode[i]
					when '0'..'9', 'a'..'f', 'A'..'F'  then
					else
						Result := False
					end
					i := i + 1
				end
			end
		end

	extra_elements: BOOLEAN
			-- has more elements?
		local
			c: like actual
		do
			if has_next then
				next
			end
			from
				c := actual
			until
				c /= ' ' or c /= '%R' or c /= '%U' or c /= '%T' or c /= '%N' or not has_next
			loop
				next
			end
			Result := has_next
		end

	is_valid_start_symbol : BOOLEAN
			-- expecting `{' or `[' as start symbol
		do
			if attached representation as s and then s.count > 0 then
				Result := s[1] = '{' or s[1] = '['
			end
		end

feature {NONE} -- Constants

	false_id: STRING = "false"

	true_id: STRING = "true"

	null_id: STRING = "null"


end
