Fixed various issue with parsing string (such as \t and related),
Implemented escaping of slash '/' only in case of '</' to avoid potential issue with javascript and </script> Many feature renaming to match Eiffel style and naming convention, kept previous feature as obsolete. Restructured the library to make easy extraction of "converter" classes if needed in the future. Updated part of the code to use new feature names.
This commit is contained in:
640
library/parser/json_parser.e
Normal file
640
library/parser/json_parser.e
Normal file
@@ -0,0 +1,640 @@
|
||||
note
|
||||
description: "Parse serialized JSON data"
|
||||
author: "jvelilla"
|
||||
date: "2008/08/24"
|
||||
revision: "Revision 0.1"
|
||||
|
||||
class
|
||||
JSON_PARSER
|
||||
|
||||
inherit
|
||||
JSON_READER
|
||||
rename
|
||||
make as make_reader,
|
||||
reset as reset_reader
|
||||
end
|
||||
|
||||
JSON_TOKENS
|
||||
|
||||
create
|
||||
make_with_string,
|
||||
make_parser
|
||||
|
||||
feature {NONE} -- Initialize
|
||||
|
||||
make_with_string (a_content: STRING)
|
||||
-- Initialize parser with JSON content `a_content'.
|
||||
require
|
||||
a_content_not_empty: a_content /= Void and then not a_content.is_empty
|
||||
do
|
||||
create errors.make
|
||||
make_reader (a_content)
|
||||
reset
|
||||
end
|
||||
|
||||
make_parser (a_json: STRING)
|
||||
-- Initialize.
|
||||
obsolete
|
||||
"Use `make_with_string' [sept/2014]."
|
||||
do
|
||||
make_with_string (a_json)
|
||||
end
|
||||
|
||||
feature -- Status report
|
||||
|
||||
is_parsed: BOOLEAN
|
||||
-- Is parsed ?
|
||||
|
||||
is_valid: BOOLEAN
|
||||
-- Is valid?
|
||||
do
|
||||
Result := not has_error
|
||||
end
|
||||
|
||||
has_error: BOOLEAN
|
||||
-- Has error?
|
||||
|
||||
errors: LINKED_LIST [STRING]
|
||||
-- Current errors
|
||||
|
||||
errors_as_string: STRING
|
||||
-- String representation of `errors'.
|
||||
do
|
||||
create Result.make_empty
|
||||
across
|
||||
errors as ic
|
||||
loop
|
||||
Result.append_string (ic.item)
|
||||
Result.append_character ('%N')
|
||||
end
|
||||
end
|
||||
|
||||
current_errors: STRING
|
||||
-- Current errors as string
|
||||
obsolete
|
||||
"USe errors_as_string [sept/2014]"
|
||||
do
|
||||
Result := errors_as_string
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
parsed_json_value: detachable JSON_VALUE
|
||||
-- Parsed json value if any.
|
||||
require
|
||||
is_parsed: is_parsed
|
||||
attribute
|
||||
ensure
|
||||
attached_result_if_valid: is_valid implies Result /= Void
|
||||
end
|
||||
|
||||
parsed_json_object: detachable JSON_OBJECT
|
||||
-- parsed json value as a JSON_OBJECT if it is an object.
|
||||
require
|
||||
is_parsed: is_parsed
|
||||
do
|
||||
if attached {JSON_OBJECT} parsed_json_value as j_object then
|
||||
Result := j_object
|
||||
end
|
||||
end
|
||||
|
||||
parsed_json_array: detachable JSON_ARRAY
|
||||
-- parsed json value as a JSON_OBJECT if it is an array.
|
||||
require
|
||||
is_parsed: is_parsed
|
||||
do
|
||||
if attached {JSON_ARRAY} parsed_json_value as j_array then
|
||||
Result := j_array
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Commands
|
||||
|
||||
reset
|
||||
-- Reset parsing values.
|
||||
do
|
||||
parsed_json_value := Void
|
||||
errors.wipe_out
|
||||
has_error := False
|
||||
is_parsed := False
|
||||
end
|
||||
|
||||
parse_content
|
||||
-- Parse JSON content `representation'.
|
||||
-- start ::= object | array
|
||||
do
|
||||
reset
|
||||
reset_reader
|
||||
|
||||
if is_valid_start_symbol then
|
||||
parsed_json_value := next_json_value
|
||||
if extra_elements then
|
||||
report_error ("Remaining element outside the main json value!")
|
||||
end
|
||||
else
|
||||
report_error ("Syntax error unexpected token, expecting `{' or `['")
|
||||
end
|
||||
is_parsed := is_valid
|
||||
end
|
||||
|
||||
|
||||
feature -- Element change
|
||||
|
||||
report_error (e: STRING)
|
||||
-- Report error `e'
|
||||
require
|
||||
e_not_void: e /= Void
|
||||
do
|
||||
has_error := True
|
||||
errors.force (e)
|
||||
ensure
|
||||
has_error: has_error
|
||||
is_not_valid: not is_valid
|
||||
end
|
||||
|
||||
feature -- Obsolete commands
|
||||
|
||||
parse_json: detachable JSON_VALUE
|
||||
-- Parse JSON data `representation'
|
||||
-- start ::= object | array
|
||||
obsolete
|
||||
"Use `parse_content' and `parsed_json_value' [sept/2014]."
|
||||
do
|
||||
parse_content
|
||||
if is_parsed then
|
||||
Result := parsed_json_value
|
||||
end
|
||||
end
|
||||
|
||||
parse: detachable JSON_VALUE
|
||||
-- Next JSON value from current position on `representation'.
|
||||
obsolete
|
||||
"Use restricted `next_parsed_json_value' [sept/2014]."
|
||||
do
|
||||
Result := next_parsed_json_value
|
||||
end
|
||||
|
||||
feature {JSON_PARSER_ACCESS} -- Obsolete commands: restricted area
|
||||
|
||||
next_parsed_json_value: detachable JSON_VALUE
|
||||
-- Return next json value from current position.
|
||||
--| this does not call `reset_reader'.
|
||||
do
|
||||
reset
|
||||
Result := next_json_value
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation: parsing
|
||||
|
||||
next_json_value: detachable JSON_VALUE
|
||||
-- Next JSON value from current position on `representation'.
|
||||
local
|
||||
c: CHARACTER
|
||||
do
|
||||
if not has_error then
|
||||
skip_white_spaces
|
||||
c := actual
|
||||
inspect c
|
||||
when token_object_open then
|
||||
Result := parse_object
|
||||
when token_double_quote then
|
||||
Result := parse_string
|
||||
when token_array_open then
|
||||
Result := parse_array
|
||||
else
|
||||
if c.is_digit or c = token_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_true
|
||||
next
|
||||
next
|
||||
next
|
||||
elseif is_false then
|
||||
Result := create {JSON_BOOLEAN}.make_false
|
||||
next
|
||||
next
|
||||
next
|
||||
next
|
||||
else
|
||||
report_error ("JSON is not well formed in parse")
|
||||
Result := Void
|
||||
end
|
||||
end
|
||||
end
|
||||
ensure
|
||||
is_parsed_implies_result_not_void: not has_error 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 = token_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 = token_colon then --| token_colon = ':'
|
||||
next
|
||||
skip_white_spaces
|
||||
else
|
||||
report_error ("%N Input string is a not well formed JSON, expected: : found: " + actual.out)
|
||||
has_more := False
|
||||
end
|
||||
l_value := next_json_value
|
||||
if not has_error and then (l_value /= Void and l_json_string /= Void) then
|
||||
Result.put (l_value, l_json_string)
|
||||
next
|
||||
skip_white_spaces
|
||||
if actual = token_object_close then
|
||||
has_more := False
|
||||
elseif actual /= token_comma then
|
||||
has_more := 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 = token_double_quote then
|
||||
from
|
||||
has_more := True
|
||||
until
|
||||
not has_more
|
||||
loop
|
||||
next
|
||||
c := actual
|
||||
if c = token_double_quote then
|
||||
has_more := False
|
||||
elseif c = '%H' then -- '%H' = '\' = reverse solidus
|
||||
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
|
||||
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
|
||||
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
|
||||
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_from_escaped_json_string (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_empty
|
||||
-- check if is an empty array []
|
||||
next
|
||||
skip_white_spaces
|
||||
if actual = token_array_close then
|
||||
-- is an empty array
|
||||
else
|
||||
previous
|
||||
from
|
||||
flag := True
|
||||
until
|
||||
not flag
|
||||
loop
|
||||
next
|
||||
skip_white_spaces
|
||||
l_value := next_json_value
|
||||
if not has_error and then l_value /= Void then
|
||||
Result.add (l_value)
|
||||
next
|
||||
skip_white_spaces
|
||||
c := actual
|
||||
if c = token_array_close then
|
||||
flag := False
|
||||
elseif c /= token_comma then
|
||||
flag := 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 = token_comma 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
|
||||
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 = token_minus 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 = token_dot 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 is_exp_token (c) then
|
||||
--| "[eE][+-]?\d+"
|
||||
s.extend (c)
|
||||
i := i + 1
|
||||
c := a_number [i]
|
||||
if c = token_plus or c = token_minus 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 the 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] = token_object_open or s [1] = token_array_open
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Constants
|
||||
|
||||
false_id: STRING = "false"
|
||||
|
||||
true_id: STRING = "true"
|
||||
|
||||
null_id: STRING = "null"
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user