note
	description: "[
			Implementation of URI Template  RFC6570.

			See http://tools.ietf.org/html/rfc6570
		]"
	legal: "See notice at end of class."
	status: "See notice at end of class."
	date: "$Date$"
	revision: "$Revision$"

class
	URI_TEMPLATE

inherit
	HASHABLE

	DEBUG_OUTPUT

create
	make

create {URI_TEMPLATE}
	make_from_uri_template

convert
	make ({READABLE_STRING_8, STRING_8})

feature {NONE} -- Initialization

	make (s: READABLE_STRING_8)
		do
			template := s
		end

	make_from_uri_template (a_tpl: like Current)
		do
			template := a_tpl.template.string
		end

feature -- Access

	template: READABLE_STRING_8
			-- URI string representation

	duplicate: like Current
			-- Duplicate object from Current
		do
			create Result.make_from_uri_template (Current)
		end

feature -- Element change

	set_template (t: like template)
			-- Set `template' to `t'
		do
			template := t
			reset
		end

feature -- Status report

	debug_output: STRING
			-- String that should be displayed in debugger to represent `Current'.
		do
			create Result.make_from_string (template)
		end

feature -- Access

	hash_code: INTEGER
			-- Hash code value
		do
			Result := template.hash_code
		end

feature -- Structures

	variable_names: LIST [STRING]
			-- All variable names
		do
			analyze
			if attached expressions as l_expressions then
				create {ARRAYED_LIST [STRING]} Result.make (l_expressions.count)
				from
					l_expressions.start
				until
					l_expressions.after
				loop
					Result.append (l_expressions.item.variable_names)
					l_expressions.forth
				end
			else
				create {ARRAYED_LIST [STRING]} Result.make (0)
			end
		end

	path_variable_names: LIST [STRING]
			-- All variable names part of the path
		do
			analyze
			if attached expressions as l_expressions then
				create {ARRAYED_LIST [STRING]} Result.make (l_expressions.count)
				from
					l_expressions.start
				until
					l_expressions.after
				loop
					if not l_expressions.item.is_query then
						Result.append (l_expressions.item.variable_names)
					end
					l_expressions.forth
				end
			else
				create {ARRAYED_LIST [STRING]} Result.make (0)
			end
		end

	query_variable_names: LIST [STRING]
			-- All variable names part of the query (i.e after '?')	
		do
			analyze
			if attached expressions as l_expressions then
				create {ARRAYED_LIST [STRING]} Result.make (l_expressions.count)
				from
					l_expressions.start
				until
					l_expressions.after
				loop
					if l_expressions.item.is_query then
						Result.append (l_expressions.item.variable_names)
					end
					l_expressions.forth
				end
			else
				create {ARRAYED_LIST [STRING]} Result.make (0)
			end
		end

feature -- Builder

	expanded_string (a_ht: HASH_TABLE [detachable ANY, STRING]): STRING
			-- Expanded template using variable from `a_ht'
		local
			tpl: like template
			exp: URI_TEMPLATE_EXPRESSION
			p,q: INTEGER
		do
			analyze
			tpl := template
			if attached expressions as l_expressions then
				create Result.make (tpl.count)
				from
					l_expressions.start
					p := 1
				until
					l_expressions.after
				loop
					q := l_expressions.item.position
						--| Added inter variable text
					Result.append (tpl.substring (p, q - 1))
						--| Expand variables ...
					exp := l_expressions.item
					exp.append_expanded_to_string (a_ht, Result)

					p := q + l_expressions.item.expression.count + 2

					l_expressions.forth
				end
				Result.append (tpl.substring (p, tpl.count))
			else
				create Result.make_from_string (tpl)
			end
		end

	expanded_string_with_base_url (a_base_url: READABLE_STRING_8; a_ht: HASH_TABLE [detachable ANY, STRING]): STRING
			-- Expanded template using variable from `a_ht'
			-- with based url
		do
			Result := a_base_url + expanded_string (a_ht)
		end

feature -- Match

	match (a_uri: READABLE_STRING_8): detachable URI_TEMPLATE_MATCH_RESULT
		require
			is_valid: is_valid
		local
			b: BOOLEAN
			tpl: like template
			l_offset: INTEGER
			p,q,nb: INTEGER
			exp: URI_TEMPLATE_EXPRESSION
			vn, s,t: STRING
			vv, path_vv: STRING
			l_vars, l_path_vars, l_query_vars: HASH_TABLE [READABLE_STRING_8, READABLE_STRING_8]
			l_uri_count: INTEGER
			tpl_count: INTEGER
			l_next_literal_separator: detachable STRING
		do
				--| Extract expansion parts  "\\{([^\\}]*)\\}"
			analyze
			if attached expressions as l_expressions then
				create l_path_vars.make (l_expressions.count)
				create l_query_vars.make (l_expressions.count)
				l_vars := l_path_vars
				b := True
				l_uri_count := a_uri.count
				tpl := template
				tpl_count := tpl.count
				if l_expressions.is_empty then
--					b := a_uri.substring (1, tpl_count).same_string (tpl)
					b := a_uri.same_string (tpl)
				else
					from
						l_expressions.start
						p := 0
						l_offset := 0
					until
						l_expressions.after or not b
					loop
						exp := l_expressions.item
						vn := exp.expression
						q := exp.position
							--| Check text between vars
						if p = q then
							--| There should be at least one literal between two expression
							--|  {var}{foobar}  is ambigous for matching ...
							--| unless with {/var} or {?var} ... since we can search for '/' or '?'
							exp.analyze
							b := exp.operator = '/' or exp.operator = '?'
							p := exp.end_position
						elseif q > p then
							if p = 0 then
								p := 1
							end
							t := tpl.substring (p, q - 1)
							s := a_uri.substring (p + l_offset, q + l_offset - 1)
							b := s.same_string (t)
							p := exp.end_position
						end
						l_expressions.forth --| we forth `l_expressions' so be careful

							--| Check related variable
						if b and then not vn.is_empty then
							if exp.is_query then
								l_vars := l_query_vars
							else
								l_vars := l_path_vars
							end
							if q + l_offset <= l_uri_count then
								inspect vn[1]
								when '?' then
									import_form_style_parameters_into (a_uri.substring (q + l_offset + 1, l_uri_count), l_vars)
									p := tpl_count + 1
									l_offset := l_offset + (l_uri_count - (q + l_offset + 1))
								when ';' then
									import_path_style_parameters_into (a_uri.substring (q + l_offset, l_uri_count), l_vars)
									p := tpl_count + 1
								else
									if not l_expressions.after then
										exp := l_expressions.item --| We change `exp' here
										exp.analyze
										if exp.operator = '/' or exp.operator = '?' then
											l_next_literal_separator := Void
										else
											l_next_literal_separator := tpl.substring (p, exp.position -1)
										end
									elseif p < tpl_count then
										l_next_literal_separator := tpl.substring (p, tpl_count)
									else
										l_next_literal_separator := Void
									end
									if vn[1] = '/' then
										vn := vn.substring (2, vn.count)
										from
											create path_vv.make_empty
											vv := "/"
											nb := 0
										until
											vv.is_empty or q + l_offset + 1 > a_uri.count
										loop
											vv := next_path_variable_value (a_uri, q + l_offset + 1, l_next_literal_separator)
											l_offset := l_offset + vv.count + 1
											nb := nb + 1
											if not vv.is_empty then
												path_vv.extend ('/')
												path_vv.append (vv)
												l_vars.force (vv, vn + "[" + nb.out + "]")
											end
										end
										l_vars.force (path_vv, vn)
										l_offset := l_offset - (1 + vn.count + 2)
									else
										vv := next_path_variable_value (a_uri, q + l_offset, l_next_literal_separator)
										l_vars.force (vv, vn)
										l_offset := l_offset + vv.count - (vn.count + 2)
									end
								end
							else
								b := exp.is_query --| query are optional
							end
						end
						if b and l_expressions.after then
							if
								(p < tpl_count) or
								(p + l_offset < l_uri_count)
							then
									--| Remaining literal part
								t := tpl.substring (p, tpl_count)
								s := a_uri.substring (p + l_offset, l_uri_count)
								b := s.same_string (t)
							end
						end
					end
				end
				if b then
					create Result.make (l_path_vars, l_query_vars)
				end
			end
		end

feature -- Basic operation

	parse
			-- Parse template
		do
			reset
			analyze
		end

feature -- Status report

	is_valid: BOOLEAN
			-- Is Current URI template valid?
		do
			analyze
			Result := not has_syntax_error
		end

feature {NONE} -- Internal Access

	reset
		do
			expressions := Void
			has_syntax_error := False
		end

	has_syntax_error: BOOLEAN
			-- Has syntax error
			--| Make sense only if `analyze' was processed before

	expressions: detachable LIST [URI_TEMPLATE_EXPRESSION]
			-- Expansion parts

feature {NONE} -- Implementation

	analyze
		local
			l_expressions: like expressions
			c: CHARACTER
			i,p,n: INTEGER
			tpl: like template
			in_x: BOOLEAN
			in_query: BOOLEAN
			x: STRING
			exp: URI_TEMPLATE_EXPRESSION
			l_has_query_expression: BOOLEAN
		do
			l_expressions := expressions
			if l_expressions = Void then
				tpl := template

					--| Extract expansion parts  "\\{([^\\}]*)\\}"
				create {ARRAYED_LIST [like expressions.item]} l_expressions.make (tpl.occurrences ('{'))
				from
					i := 1
					n := tpl.count
					l_has_query_expression := False
					create x.make_empty
				until
					i > n
				loop
					c := tpl[i]
					if in_x then
						if c = '}' then
							create exp.make (p, x.twin, in_query)
							l_expressions.force (exp)
							x.wipe_out
							in_x := False
							if l_has_query_expression and then i < n then
								--| Remaining text after {?exp}
								has_syntax_error := True
							end
						else
							x.extend (c)
						end
					else
						inspect c
						when '{' then
							check x_is_empty: x.is_empty end
							p := i
							in_x := True
							if not l_has_query_expression then
								l_has_query_expression := tpl.valid_index (i+1) and then tpl[i+1] = '?'
							end
							if not in_query then
								in_query := l_has_query_expression
							end
						when '?' then
							in_query := True
						else
						end
					end
					i := i + 1
				end
				expressions := l_expressions
			end
		end

	import_path_style_parameters_into (a_content: STRING; res: HASH_TABLE [READABLE_STRING_8, READABLE_STRING_8])
		require
			a_content_attached: a_content /= Void
			res_attached: res /= Void
		do
			import_custom_style_parameters_into (a_content, ';', res)
		end

	import_form_style_parameters_into (a_content: STRING; res: HASH_TABLE [READABLE_STRING_8, READABLE_STRING_8])
		require
			a_content_attached: a_content /= Void
			res_attached: res /= Void
		do
			import_custom_style_parameters_into (a_content, '&', res)
		end

	import_custom_style_parameters_into (a_content: STRING; a_separator: CHARACTER; res: HASH_TABLE [READABLE_STRING_8, READABLE_STRING_8])
		require
			a_content_attached: a_content /= Void
			res_attached: res /= Void
		local
			n, p, i, j: INTEGER
			s: READABLE_STRING_8
			l_name, l_value: READABLE_STRING_8
		do
			n := a_content.count
			if n > 0 then
				from
					p := 1
				until
					p = 0
				loop
					i := a_content.index_of (a_separator, p)
					if i = 0 then
						s := a_content.substring (p, n)
						p := 0
					else
						s := a_content.substring (p, i - 1)
						p := i + 1
					end
					if not s.is_empty then
						j := s.index_of ('=', 1)
						if j > 0 then
							l_name := s.substring (1, j - 1)
							l_value := s.substring (j + 1, s.count)
							res.force (l_value, l_name)
						end
					end
				end
			end
		end

	next_path_variable_value (a_uri: STRING; a_index: INTEGER; a_end_token: detachable STRING): STRING
		require
			valid_index: a_index <= a_uri.count
		local
			c: CHARACTER
			i,n,p: INTEGER
			l_end_token_first_char: CHARACTER
			l_end_token_count: INTEGER
		do
			from
				if a_end_token /= Void and then not a_end_token.is_empty then
					l_end_token_first_char := a_end_token.item (1)
					l_end_token_count := a_end_token.count
				end
				i := a_index
				n := a_uri.count
			until
				i > n
			loop
				c := a_uri[i]
				inspect c
				when '/', '?' then
					i := n
				else
					if
						a_end_token /= Void and then
						c = l_end_token_first_char and then
						a_uri.substring (i, i + l_end_token_count - 1).same_string (a_end_token)
					then
						i := n
					else
						p := i
					end
				end
				i := i + 1
			end
			Result := a_uri.substring (a_index, p)
		end

note
	copyright: "2011-2012, 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
