note
	description: "[
			Object parsing a .ini file.
			
			Lines starting by '#', or ';', or "--" are comments
			Section are declared using brackets "[section_name]"
			Values are declared as "key = value"
			List values are declared as multiple lines "key[] = value"
			example:
			--------------------
			[first_section]
			one = abc
			
			[second_section]
			two = def			
			lst[] = a
			lst[] = b
			lst[] = c
			
			[third_section]
			three = ghi
			table[a] = foo
			table[b] = bar
			--------------------
			
			Values are accessible from `items' or by section from `sections'
			`item' has smart support for '.'
			
			item ("one") -> abc
			item ("two") -> def
			item ("second_section.two") -> def
			item ("table[b]") -> foo
			item ("table.b") -> foo
			item ("third_section.table[b]") -> foo
			item ("third_section.table.b") -> foo

			notes:
				it is considered that the .ini file is utf-8 encoded
				keys are unicode string
				values are stored UTF-8 string, but one can use unicode_string_item to convert to STRING_32
				
			Additional features:
			- Include other files:
				@include=file-to-include

		]"
	date: "$Date: 2015-03-09 19:25:49 +0100 (lun., 09 mars 2015) $"
	revision: "$Revision: 96797 $"

class
	INI_CONFIG

inherit
	CONFIG_READER

	TABLE_ITERABLE [ANY, READABLE_STRING_GENERAL]

	SHARED_EXECUTION_ENVIRONMENT

create
	make_from_file,
	make_from_string,
	make_from_items

feature {NONE} -- Initialization

	make_from_file (p: PATH)
		do
			initialize
			parse_file (p)
		end

	make_from_string (a_content: STRING_8)
		do
			initialize
			parse_content (a_content)
		end

	make_from_items (a_items: like items)
		do
			initialize
			across
				a_items as ic
			loop
				items.force (ic.item, ic.key)
			end
		end

	initialize
			-- Initialize data.
		do
			associated_path := Void
			create utf
			create items.make (0)
			create sections.make (0)
		end

	reset
			-- Reset internal data.
		do
			has_error := False
			items.wipe_out
			sections.wipe_out
		end

feature -- Status report

	has_item  (k: READABLE_STRING_GENERAL): BOOLEAN
			-- Has item associated with key `k'?
		do
			Result := item (k) /= Void
		end

	has_error: BOOLEAN
			-- Has error?
			--| Syntax error, source not found, ...

feature -- Access: Config Reader

	text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
			-- String item associated with key `k'.	
		local
			obj: like item
		do
			obj := item (k)
			if attached {READABLE_STRING_32} obj as s32 then
				Result := s32
			elseif attached {READABLE_STRING_8} obj as s then
				Result := utf.utf_8_string_8_to_escaped_string_32 (s)
			end
		end

	integer_item (k: READABLE_STRING_GENERAL): INTEGER
			-- Integer item associated with key `k'.
		do
			if attached {READABLE_STRING_GENERAL} item (k) as s then
				Result := s.to_integer
			end
		end

	associated_path: detachable PATH
			-- If current was built from a filename, return this path.

feature -- Duplication

	sub_config (k: READABLE_STRING_GENERAL): detachable CONFIG_READER
			-- Configuration representing a subset of Current for key `k'.
		do
			if attached sections.item (k) as l_items then
				create {INI_CONFIG} Result.make_from_items (l_items)
			end
		end

feature -- Access		

	item (k: READABLE_STRING_GENERAL): detachable ANY
			-- Value associated with key `k'.
		local
			i: INTEGER
			s,sk: detachable READABLE_STRING_GENERAL
		do
				-- Try first directly in values
			Result := items.item (k)
			if Result = Void then
					--| If there is a dot
					--| this could be
					--|		section.variable
					--|		variable.name or variable[name]
				i := k.index_of ('.', 1)
				if i > 0 then
					s := k.head (i - 1)
						-- then search first in section
					if attached sections.item (s) as l_section then
						sk := k.substring (i + 1, k.count)
						Result := l_section.item (sk)
						if Result = Void then
							Result := item_from_values (l_section, sk)
						end
					end
					if Result = Void then
						i := k.index_of ('.', i + 1)
						if i > 0 then
								-- There is another dot .. could be due to include
							across
								sections as ic
							until
								Result /= Void
							loop
								s := ic.key
									-- If has other dot, this may be in sub sections ...
								if s.has ('.') and then k.starts_with (s) and then k[s.count + 1] = '.' then
									if attached sections.item (s) as l_section then
										sk := k.substring (s.count + 2, k.count)
										Result := l_section.item (sk)
										if Result = Void then
											Result := item_from_values (l_section, sk)
										end
									end
								end
							end
						end
						if Result = Void then
								-- otherwise in values object.
							Result := item_from_values (items, k)
						end
					end
				else
						--| Could be
						--| variable[name]
					Result := item_from_values (items, k)
				end
			end
		end

	items: STRING_TABLE [ANY]

	sections: STRING_TABLE [like items]

feature -- Access

	new_cursor: TABLE_ITERATION_CURSOR [ANY, READABLE_STRING_GENERAL]
			-- Fresh cursor associated with current structure
		do
			Result := items.new_cursor
		end

feature {NONE} -- Implementation

	item_from_values (a_values: STRING_TABLE [ANY]; k: READABLE_STRING_GENERAL): detachable ANY
		local
			i,j: INTEGER
			s: READABLE_STRING_GENERAL
		do
			Result := a_values.item (k)
			if Result = Void then
				i := k.index_of ('.', 1)
				if i > 0 then
					s := k.head (i - 1)
					if attached {STRING_TABLE [ANY]} a_values.item (s) as l_table then
						Result := l_table.item (k.substring (i + 1, k.count))
					end
				else
					i := k.index_of ('[', 1)
					if i > 0 then
						j := k.index_of (']', i + 1)
						if j = k.count then
							s := k.head (i - 1)
							if attached {STRING_TABLE [ANY]} a_values.item (s) as l_table then
								Result := l_table.item (k.substring (i + 1, j - 1))
							end
						end
					end
				end
			end
		end

	record_in_section (obj: ANY; k: READABLE_STRING_GENERAL; a_section: READABLE_STRING_GENERAL)
		local
			v: detachable like items
		do
			v := sections.item (a_section)
			if v = Void then
				create v.make (1)
				sections.force (v, a_section)
			end
			v.force (obj, k)
		end

	parse_content (a_content: STRING_8)
		local
			i,j,n: INTEGER
			s: READABLE_STRING_8
		do
			last_section_name := Void
			from
				i := 1
				n := a_content.count
			until
				i > n
			loop
				j := a_content.index_of ('%N', i)
				if j > 0 then
					s := a_content.substring (i, j - 1)
					i := j + 1
					if i <= n and then a_content[i] = '%R' then
						i := i + 1
					end
				else
					j := n
					s := a_content.substring (i, j)
					i := j + 1
				end
				analyze_line (s, Void)
			variant
				i
			end
			last_section_name := Void
		rescue
			last_section_name := Void
			has_error := True
		end

	parse_file (p: PATH)
		do
			associated_path := p
			last_section_name := Void
			import_path (p, Void)
			last_section_name := Void
		end

	import_path (p: PATH; a_section_prefix: detachable READABLE_STRING_32)
			-- Import config from path `p'.
		local
			f: PLAIN_TEXT_FILE
			l_last_section_name: like last_section_name
			retried: BOOLEAN
			l_old_associated_path: like associated_path
		do
			l_old_associated_path := associated_path
			if retried then
				has_error := True
			else
				associated_path := p
				l_last_section_name := last_section_name
				last_section_name := Void
				create f.make_with_path (p)
				if f.exists and then f.is_access_readable then
					f.open_read
					from
					until
						f.exhausted or f.end_of_file
					loop
						f.read_line_thread_aware
						analyze_line (f.last_string, a_section_prefix)
					end
					f.close
				else
						-- File not readable
					has_error := True
				end
			end
			last_section_name := l_last_section_name
			associated_path := l_old_associated_path
		rescue
			retried := True
			retry
		end

	analyze_line (a_line: STRING_8; a_section_prefix: detachable READABLE_STRING_32)
			-- Analyze line `a_line'.
		local
			k,sk: STRING_32
			v: STRING_8
			obj: detachable ANY
			lst: LIST [STRING_8]
			tb: STRING_TABLE [STRING_8]
			i,j: INTEGER
			p: PATH
			l_section_name: like last_section_name
		do
			obj := Void
			a_line.left_adjust
			if
				a_line.is_empty
				or a_line.is_whitespace
				or a_line.starts_with_general ("#")
				or a_line.starts_with_general (";")
				or a_line.starts_with_general ("--")
			then
					-- Ignore
			elseif a_line.starts_with_general ("[") then
				i := a_line.index_of (']', 1)
				l_section_name := utf.utf_8_string_8_to_string_32 (a_line.substring (2, i - 1))
				l_section_name.left_adjust
				l_section_name.right_adjust
				if a_section_prefix /= Void then
					l_section_name.prepend_character ('.')
					l_section_name.prepend (a_section_prefix)
				end
				last_section_name := l_section_name
			else
				i := a_line.index_of ('=', 1)
				if i > 1 then
					k := utf.utf_8_string_8_to_string_32 (a_line.head (i - 1))
					k.right_adjust
					v := a_line.substring (i + 1, a_line.count)
					v.left_adjust
					v.right_adjust


					if k.is_case_insensitive_equal_general ("@include") then
						create p.make_from_string (v)
						if not p.is_absolute and attached associated_path as l_path then
							p := l_path.parent.extended_path (p)
						end
						import_path (p, last_section_name)
					else
						i := k.index_of ('[', 1)
						if i > 0 then
							j := k.index_of (']', i + 1)
							if j = i + 1 then -- ends_with "[]"
								k.keep_head (i - 1)
								if attached {LIST [STRING_8]} items.item (k) as l_list then
									lst := l_list
								else
									create {ARRAYED_LIST [STRING_8]} lst.make (1)
									record_item (lst, k, a_section_prefix)
								end
								lst.force (v)
								obj := lst
							elseif j > i then
									-- table
								sk := k.substring (i + 1, j - 1)
								sk.left_adjust
								sk.right_adjust
								k.keep_head (i - 1)
								if attached {STRING_TABLE [STRING_8]} items.item (k) as l_table then
									tb := l_table
								else
									create tb.make (1)
									record_item (tb, k, a_section_prefix)
								end
								tb.force (v, sk)
								obj := tb
							else
									-- Error missing closing ']'
								has_error := True
							end
						else
							record_item (v, k, a_section_prefix)
							obj := v
						end

						if attached last_section_name as l_session and then obj /= Void then
							record_in_section (obj, k, l_session)
						end
					end
				else
						-- Error
					has_error := True
				end
			end
		end

	record_item (v: ANY; k: READABLE_STRING_32; a_section_prefix: detachable READABLE_STRING_32)
		do
			if a_section_prefix /= Void then
				items.force (v, a_section_prefix + {STRING_32} "." + k)
			else
				items.force (v, k)
			end
		end

feature {NONE} -- Implementation

	last_section_name: detachable STRING_32

	utf: UTF_CONVERTER

invariant

note
	copyright: "2011-2014, Jocelyn Fiat, Eiffel Software and others"
	license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
	source: "[
			Eiffel Software
			5949 Hollister Ave., Goleta, CA 93117 USA
			Telephone 805-685-1006, Fax 805-685-6869
			Website http://www.eiffel.com
			Customer support http://support.eiffel.com
		]"
end
