note
	description: "[
			The class provides an easy way to build HTTP header.
			
			You will also find some helper feature to help coding most common usage
			
			Please, have a look at constants classes such as
				HTTP_MIME_TYPES
				HTTP_HEADER_NAMES
				HTTP_STATUS_CODE
				HTTP_REQUEST_METHODS
			(or HTTP_CONSTANTS which groups them for convenience)
			
			Note the return status code is not part of the HTTP header
		]"
	legal: "See notice at end of class."
	status: "See notice at end of class."
	date: "$Date$"
	revision: "$Revision$"

class
	HTTP_HEADER

inherit
	ANY

create
	make,
	make_with_count,
	make_from_array,
	make_from_header,
	make_from_raw_header_data

convert
	make_from_array ({ARRAY [TUPLE [key: READABLE_STRING_8; value: READABLE_STRING_8]]}),
	string: {READABLE_STRING_8, STRING_8}

feature {NONE} -- Initialization

	make
			-- Initialize current
		do
			make_with_count (3)
		end

	make_with_count (n: INTEGER)
			-- Make with a capacity of `n' header entries
		do
			create {ARRAYED_LIST [READABLE_STRING_8]} headers.make (n)
		end

	make_from_array (a_headers: ARRAY [TUPLE [key: READABLE_STRING_8; value: READABLE_STRING_8]])
			-- Create HEADER from array of pair (key,value)
		do
			if a_headers.is_empty then
				make_with_count (0)
			else
				make_with_count (a_headers.count)
				append_array (a_headers)
			end
		end

	make_from_header (a_header: HTTP_HEADER)
			-- Create Current from existing HEADER `a_header'
		do
			make_with_count (a_header.headers.count)
			append_header_object (a_header)
		end

	make_from_raw_header_data (h: READABLE_STRING_8)
			-- Create Current from raw header data
        local
        	line : detachable STRING
			lines: LIST [READABLE_STRING_8]
        do
            lines := h.split ('%N')
            make_with_count (lines.count)
            across
            	lines as c
            loop
            	line := c.item
            	if not line.is_empty then
            		if line [line.count] = '%R' then
						line.remove_tail (1)
            		end
					add_header (line)
            	end
			end
		end

feature -- Recycle

	recycle
			-- Recycle current object
		do
			headers.wipe_out
		end

feature -- Access

	headers: ARRAYED_LIST [READABLE_STRING_8]
			-- Header's lines

	string: STRING_8
			-- String representation of the headers
		local
			l_headers: like headers
		do
			l_headers := headers
			if not l_headers.is_empty then
				create Result.make (l_headers.count * 32)
				across
					l_headers as c
				loop
					append_line_to (c.item, Result)
				end
			else
				create Result.make_empty
			end
		ensure
			result_has_ending_cr_lf: Result.count >= 2 implies Result.substring (Result.count - 1, Result.count).same_string ("%R%N")
			result_has_single_ending_cr_lf: Result.count >= 4 implies not Result.substring (Result.count - 3, Result.count).same_string ("%R%N%R%N")
		end

	to_name_value_iterable: ITERABLE [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]]
		local
			res: ARRAYED_LIST [TUPLE [READABLE_STRING_8, READABLE_STRING_8]]
		do
			create res.make (headers.count)
			across
				headers as c
			loop
				if attached header_name_value (c.item) as tu then
					res.extend (tu)
				end
			end
			Result := res
		end

feature -- Header: filling

	append_array (a_headers: ARRAY [TUPLE [key: READABLE_STRING_8; value: READABLE_STRING_8]])
			-- Append array of key,value headers
		do
			headers.grow (headers.count + a_headers.count)
			across
				a_headers as c
			loop
				put_header_key_value (c.item.key, c.item.value)
			end
		end

	append_header_object (h: HTTP_HEADER)
			-- Append headers from `h'
		do
			headers.grow (headers.count + h.headers.count)
			across
				h.headers as c
			loop
				add_header (c.item.string)
			end
		end

feature -- Header change: general

	add_header (h: READABLE_STRING_8)
			-- Add header `h'
			-- if it already exists, there will be multiple header with same name
			-- which can also be valid
		require
			h_not_empty: not h.is_empty
		do
			headers.force (h)
		end

	put_header (h: READABLE_STRING_8)
			-- Add header `h' or replace existing header of same header name
		require
			h_not_empty: not h.is_empty
		do
			force_header_by_name (header_name_colon (h), h)
		end

	add_header_key_value (k,v: READABLE_STRING_8)
			-- Add header `k:v', or replace existing header of same header name/key
		do
			add_header (k + colon_space + v)
		end

	put_header_key_value (k,v: READABLE_STRING_8)
			-- Add header `k:v', or replace existing header of same header name/key
		do
			put_header (k + colon_space + v)
		end

feature -- Content related header

	put_content_type (t: READABLE_STRING_8)
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, t)
		end

	add_content_type (t: READABLE_STRING_8)
			-- same as `put_content_type', but allow multiple definition of "Content-Type"
		do
			add_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, t)
		end

	put_content_type_with_parameters (t: READABLE_STRING_8; a_params: detachable ARRAY [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]])
		local
			s: STRING_8
		do
			if a_params /= Void and then not a_params.is_empty then
				create s.make_from_string (t)
				across
					a_params as p
				loop
					if attached p.item as nv then
						s.append_character (';')
						s.append_character (' ')
						s.append (nv.name)
						s.append_character ('=')
						s.append_character ('%"')
						s.append (nv.value)
						s.append_character ('%"')
					end
				end
				put_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, s)
			else
				put_content_type (t)
			end
		end

	add_content_type_with_parameters (t: READABLE_STRING_8; a_params: detachable ARRAY [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]])
		local
			s: STRING_8
		do
			if a_params /= Void and then not a_params.is_empty then
				create s.make_from_string (t)
				across
					a_params as p
				loop
					if attached p.item as nv then
						s.append_character (';')
						s.append_character (' ')
						s.append (nv.name)
						s.append_character ('=')
						s.append_character ('%"')
						s.append (nv.value)
						s.append_character ('%"')
					end
				end
				add_header_key_value ({HTTP_HEADER_NAMES}.header_content_type, s)
			else
				add_content_type (t)
			end
		end

	put_content_type_with_charset (t: READABLE_STRING_8; c: READABLE_STRING_8)
		do
			put_content_type_with_parameters (t, <<["charset", c]>>)
		end

	add_content_type_with_charset (t: READABLE_STRING_8; c: READABLE_STRING_8)
			-- same as `put_content_type_with_charset', but allow multiple definition of "Content-Type"	
		do
			add_content_type_with_parameters (t, <<["charset", c]>>)
		end

	put_content_type_with_name (t: READABLE_STRING_8; n: READABLE_STRING_8)
		do
			put_content_type_with_parameters (t, <<["name", n]>>)
		end

	add_content_type_with_name (t: READABLE_STRING_8; n: READABLE_STRING_8)
			-- same as `put_content_type_with_name', but allow multiple definition of "Content-Type"	
		do
			add_content_type_with_parameters (t, <<["name", n]>>)
		end

	put_content_length (n: INTEGER)
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_content_length, n.out)
		end

	put_content_transfer_encoding (a_mechanism: READABLE_STRING_8)
			-- Put "Content-Transfer-Encoding" header with for instance "binary"
			--|   encoding := "Content-Transfer-Encoding" ":" mechanism
			--|
			--|   mechanism :=     "7bit"  ;  case-insensitive
			--|                  / "quoted-printable"
			--|                  / "base64"
			--|                  / "8bit"
			--|                  / "binary"
			--|                  / x-token
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_content_transfer_encoding, a_mechanism)
		end

	put_transfer_encoding (a_enc: READABLE_STRING_8)
			-- Put "Transfer-Encoding" header with for instance "chunked"
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_transfer_encoding, a_enc)
		end

	put_transfer_encoding_binary
			-- Put "Transfer-Encoding: binary" header
		do
			put_transfer_encoding (str_binary)
		end

	put_transfer_encoding_chunked
			-- Put "Transfer-Encoding: chunked" header
		do
			put_transfer_encoding (str_chunked)
		end

	put_content_disposition (a_type: READABLE_STRING_8; a_params: detachable READABLE_STRING_8)
			-- Put "Content-Disposition" header
			--| See RFC2183
			--|     disposition := "Content-Disposition" ":"
			--|                    disposition-type
			--|                    *(";" disposition-parm)
			--|     disposition-type := "inline"
			--|                       / "attachment"
			--|                       / extension-token
			--|                       ; values are not case-sensitive
			--|     disposition-parm := filename-parm
			--|                       / creation-date-parm
			--|                       / modification-date-parm
			--|                       / read-date-parm
			--|                       / size-parm
			--|                       / parameter
			--|     filename-parm := "filename" "=" value
			--|     creation-date-parm := "creation-date" "=" quoted-date-time
			--|     modification-date-parm := "modification-date" "=" quoted-date-time
			--|     read-date-parm := "read-date" "=" quoted-date-time
			--|     size-parm := "size" "=" 1*DIGIT
			--|     quoted-date-time := quoted-string
			--|                      ; contents MUST be an RFC 822 `date-time'
			--|                      ; numeric timezones (+HHMM or -HHMM) MUST be used
		do
			if a_params /= Void then
				put_header_key_value ({HTTP_HEADER_NAMES}.header_content_disposition, a_type + semi_colon_space + a_params)
			else
				put_header_key_value ({HTTP_HEADER_NAMES}.header_content_disposition, a_type)
			end
		end

feature -- Content-type helpers

	put_content_type_text_css				do put_content_type ({HTTP_MIME_TYPES}.text_css) end
	put_content_type_text_csv				do put_content_type ({HTTP_MIME_TYPES}.text_csv) end
	put_content_type_text_html				do put_content_type ({HTTP_MIME_TYPES}.text_html) end
	put_content_type_text_javascript		do put_content_type ({HTTP_MIME_TYPES}.text_javascript) end
	put_content_type_text_json				do put_content_type ({HTTP_MIME_TYPES}.text_json) end
	put_content_type_text_plain				do put_content_type ({HTTP_MIME_TYPES}.text_plain) end
	put_content_type_text_xml				do put_content_type ({HTTP_MIME_TYPES}.text_xml) end

	put_content_type_application_json		do put_content_type ({HTTP_MIME_TYPES}.application_json) end
	put_content_type_application_javascript	do put_content_type ({HTTP_MIME_TYPES}.application_javascript) end
	put_content_type_application_zip		do put_content_type ({HTTP_MIME_TYPES}.application_zip)	end
	put_content_type_application_pdf		do put_content_type ({HTTP_MIME_TYPES}.application_pdf) end

	put_content_type_image_gif				do put_content_type ({HTTP_MIME_TYPES}.image_gif) end
	put_content_type_image_png				do put_content_type ({HTTP_MIME_TYPES}.image_png) end
	put_content_type_image_jpg				do put_content_type ({HTTP_MIME_TYPES}.image_jpg) end
	put_content_type_image_svg_xml			do put_content_type ({HTTP_MIME_TYPES}.image_svg_xml) end

	put_content_type_message_http			do put_content_type ({HTTP_MIME_TYPES}.message_http) end

	put_content_type_multipart_mixed		do put_content_type ({HTTP_MIME_TYPES}.multipart_mixed) end
	put_content_type_multipart_alternative	do put_content_type ({HTTP_MIME_TYPES}.multipart_alternative) end
	put_content_type_multipart_related		do put_content_type ({HTTP_MIME_TYPES}.multipart_related) end
	put_content_type_multipart_form_data	do put_content_type ({HTTP_MIME_TYPES}.multipart_form_data) end
	put_content_type_multipart_signed		do put_content_type ({HTTP_MIME_TYPES}.multipart_signed) end
	put_content_type_multipart_encrypted	do put_content_type ({HTTP_MIME_TYPES}.multipart_encrypted) end
	put_content_type_application_x_www_form_encoded	do put_content_type ({HTTP_MIME_TYPES}.application_x_www_form_encoded) end

feature -- Date

	put_date (s: READABLE_STRING_8)
			-- Put "Date: " header
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_date, s)
		end

	put_current_date
			-- Put current date time with "Date" header
		do
			put_utc_date (create {DATE_TIME}.make_now_utc)
		end

	put_utc_date (a_utc_date: DATE_TIME)
			-- Put UTC date time `dt' with "Date" header
		do
			put_date (date_to_rfc1123_http_date_format (a_utc_date))
		end

	put_last_modified (a_utc_date: DATE_TIME)
			-- Put UTC date time `dt' with "Date" header
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_last_modified, date_to_rfc1123_http_date_format (a_utc_date))
		end

feature -- Others	

	put_expires (sec: INTEGER)
		do
			put_expires_string (sec.out)
		end

	put_expires_string (s: STRING)
		do
			put_header_key_value ("Expires", s)
		end

	put_expires_date (a_utc_date: DATE_TIME)
		do
			put_header_key_value ("Expires", date_to_rfc1123_http_date_format (a_utc_date))
		end

	put_cache_control (s: READABLE_STRING_8)
			-- `s' could be for instance "no-cache, must-revalidate"
		do
			put_header_key_value ("Cache-Control", s)
		end

	put_pragma (s: READABLE_STRING_8)
		do
			put_header_key_value ("Pragma", s)
		end

	put_pragma_no_cache
		do
			put_pragma ("no-cache")
		end

feature -- Redirection

	put_location (a_location: READABLE_STRING_8)
			-- Tell the client the new location `a_location'
		require
			a_location_valid: not a_location.is_empty
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_location, a_location)
		end

	put_refresh (a_location: READABLE_STRING_8; a_timeout_in_seconds: INTEGER)
			-- Tell the client to refresh page with `a_location' after `a_timeout_in_seconds' in seconds
		require
			a_location_valid: not a_location.is_empty
		do
			put_header_key_value ({HTTP_HEADER_NAMES}.header_refresh, a_timeout_in_seconds.out + "; url=" + a_location)
		end

feature -- Cookie

	put_cookie (key, value: READABLE_STRING_8; expiration, path, domain: detachable READABLE_STRING_8; secure, http_only: BOOLEAN)
			-- Set a cookie on the client's machine
			-- with key 'key' and value 'value'.
			-- Note: you should avoid using "localhost" as `domain' for local cookies
			--       since they are not always handled by browser (for instance Chrome)
		require
			make_sense: (key /= Void and value /= Void) and then (not key.is_empty and not value.is_empty)
			domain_without_port_info: domain /= Void implies domain.index_of (':', 1) = 0
		local
			s: STRING
		do
			s := {HTTP_HEADER_NAMES}.header_set_cookie + colon_space + key + "=" + value
			if
				domain /= Void and then not domain.same_string ("localhost")
			then
				s.append ("; Domain=" + domain)
			end
			if path /= Void then
				s.append ("; Path=" + path)
			end
			if expiration /= Void then
				s.append ("; Expires=" + expiration)
			end
			if secure then
				s.append ("; Secure")
			end
			if http_only then
				s.append ("; HttpOnly")
			end
			add_header (s)
		end

	put_cookie_with_expiration_date (key, value: READABLE_STRING_8; expiration: DATE_TIME; path, domain: detachable READABLE_STRING_8; secure, http_only: BOOLEAN)
			-- Set a cookie on the client's machine
			-- with key 'key' and value 'value'.
		require
			make_sense: (key /= Void and value /= Void) and then (not key.is_empty and not value.is_empty)
		do
			put_cookie (key, value, date_to_rfc1123_http_date_format (expiration), path, domain, secure, http_only)
		end

feature -- Status report

	header_named_value (a_name: READABLE_STRING_8): detachable STRING_8
			-- Has header item for `n'?
		require
			has_header: has_header_named (a_name)
		local
			c: like headers.new_cursor
			n: INTEGER
			l_line: READABLE_STRING_8
		do
			from
				n := a_name.count
				c := headers.new_cursor
			until
				c.after or Result /= Void
			loop
				l_line := c.item
				if l_line.starts_with (a_name) then
					if l_line.valid_index (n + 1) then
						if l_line [n + 1] = ':' then
							Result := l_line.substring (n + 2, l_line.count)
							Result.left_adjust
							Result.right_adjust
						end
					end
				end
				c.forth
			end
		end

	has_header_named (a_name: READABLE_STRING_8): BOOLEAN
			-- Has header item for `n'?
		local
			c: like headers.new_cursor
			n: INTEGER
			l_line: READABLE_STRING_8
		do
			from
				n := a_name.count
				c := headers.new_cursor
			until
				c.after or Result
			loop
				l_line := c.item
				if l_line.starts_with (a_name) then
					if l_line.valid_index (n + 1) then
						Result := l_line [n + 1] = ':'
					end
				end
				c.forth
			end
		end

	has_content_length: BOOLEAN
			-- Has header "Content-Length"
		do
			Result := has_header_named ({HTTP_HEADER_NAMES}.header_content_length)
		end

	has_content_type: BOOLEAN
			-- Has header "Content-Type"
		do
			Result := has_header_named ({HTTP_HEADER_NAMES}.header_content_type)
		end

	has_transfer_encoding_chunked: BOOLEAN
			-- Has "Transfer-Encoding: chunked" header
		do
			if has_header_named ({HTTP_HEADER_NAMES}.header_transfer_encoding) then
				Result := attached header_named_value ({HTTP_HEADER_NAMES}.header_transfer_encoding) as v and then v.same_string (str_chunked)
			end
		end


feature {NONE} -- Implementation: Header

	force_header_by_name (n: detachable READABLE_STRING_8; h: READABLE_STRING_8)
			-- Add header `h' or replace existing header of same header name `n'
		require
			h_has_name_n: (n /= Void and attached header_name_colon (h) as hn) implies n.same_string (hn)
		local
			l_headers: like headers
		do
			if n /= Void then
				from
					l_headers := headers
					l_headers.start
				until
					l_headers.after or l_headers.item.starts_with (n)
				loop
					l_headers.forth
				end
				if not l_headers.after then
					l_headers.replace (h)
				else
					add_header (h)
				end
			else
				add_header (h)
			end
		end

	header_name_colon (h: READABLE_STRING_8): detachable STRING_8
			-- If any, header's name with colon
			--| ex: for "Foo-bar: something", this will return "Foo-bar:"
		local
			s: detachable STRING_8
			i,n: INTEGER
			c: CHARACTER
		do
			from
				i := 1
				n := h.count
				create s.make (10)
			until
				i > n or c = ':' or s = Void
			loop
				c := h[i]
				inspect c
				when ':' then
					s.extend (c)
				when '-', 'a' .. 'z', 'A' .. 'Z' then
					s.extend (c)
				else
					s := Void
				end
				i := i + 1
			end
			Result := s
		end

	header_name_value (h: READABLE_STRING_8): detachable TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]
			-- If any, header's [name,value]
			--| ex: for "Foo-bar: something", this will return ["Foo-bar", "something"]
		local
			s: detachable STRING_8
			i,n: INTEGER
			c: CHARACTER
		do
			from
				i := 1
				n := h.count
				create s.make (10)
			until
				i > n or c = ':' or s = Void
			loop
				c := h[i]
				inspect c
				when ':' then
				when '-', 'a' .. 'z', 'A' .. 'Z' then
					s.extend (c)
				else
					s := Void
				end
				i := i + 1
			end
			if s /= Void then
				Result := [s, h.substring (i, n)]
			end
		end

feature {NONE} -- Implementation

	append_line_to (s: READABLE_STRING_8; h: STRING_8)
		do
			h.append_string (s)
			append_end_of_line_to (h)
		end

	append_end_of_line_to (h: STRING_8)
		do
			h.append_character ('%R')
			h.append_character ('%N')
		end

	date_to_rfc1123_http_date_format (dt: DATE_TIME): STRING_8
			-- String representation of `dt' using the RFC 1123
		do
			Result := dt.formatted_out ("ddd, [0]dd mmm yyyy [0]hh:[0]mi:[0]ss") + " GMT"
		end

feature {NONE} -- Constants

	str_binary: STRING = "binary"
	str_chunked: STRING = "chunked"

	colon_space: STRING = ": "
	semi_colon_space: STRING = "; "

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
