From b58e4e19e15b208294da34ce1c5c6a76a9869436 Mon Sep 17 00:00:00 2001 From: jvelilla Date: Thu, 17 Nov 2011 12:54:35 +0100 Subject: [PATCH] Initial import CONNEG library, support server side content negotiation. --- library/protocol/CONNEG/.gitignore | 1 + library/protocol/CONNEG/README.md | 5 + library/protocol/CONNEG/library/.gitignore | 4 + .../library/common_accept_header_parser.e | 282 ++++++++++++++ .../protocol/CONNEG/library/common_results.e | 119 ++++++ .../protocol/CONNEG/library/conneg-safe.ecf | 18 + library/protocol/CONNEG/library/conneg.ecf | 19 + .../CONNEG/library/fitness_and_quality.e | 81 ++++ .../protocol/CONNEG/library/language_parse.e | 352 ++++++++++++++++++ .../CONNEG/library/language_results.e | 143 +++++++ library/protocol/CONNEG/library/license.lic | 4 + library/protocol/CONNEG/library/mime_parse.e | 349 +++++++++++++++++ .../protocol/CONNEG/library/parse_results.e | 144 +++++++ .../protocol/CONNEG/library/shared_conneg.e | 30 ++ library/protocol/CONNEG/library/variants.e | 76 ++++ library/protocol/CONNEG/run_test.rb | 79 ++++ library/protocol/CONNEG/test/.gitignore | 4 + library/protocol/CONNEG/test/application.e | 87 +++++ .../test/common_accept_header_parser_test.e | 57 +++ .../CONNEG/test/language_parser_test.e | 117 ++++++ .../protocol/CONNEG/test/mime_parser_test.e | 129 +++++++ library/protocol/CONNEG/test/test-safe.ecf | 20 + library/protocol/CONNEG/test/test.ecf | 21 ++ 23 files changed, 2141 insertions(+) create mode 100644 library/protocol/CONNEG/.gitignore create mode 100644 library/protocol/CONNEG/README.md create mode 100644 library/protocol/CONNEG/library/.gitignore create mode 100644 library/protocol/CONNEG/library/common_accept_header_parser.e create mode 100644 library/protocol/CONNEG/library/common_results.e create mode 100644 library/protocol/CONNEG/library/conneg-safe.ecf create mode 100644 library/protocol/CONNEG/library/conneg.ecf create mode 100644 library/protocol/CONNEG/library/fitness_and_quality.e create mode 100644 library/protocol/CONNEG/library/language_parse.e create mode 100644 library/protocol/CONNEG/library/language_results.e create mode 100644 library/protocol/CONNEG/library/license.lic create mode 100644 library/protocol/CONNEG/library/mime_parse.e create mode 100644 library/protocol/CONNEG/library/parse_results.e create mode 100644 library/protocol/CONNEG/library/shared_conneg.e create mode 100644 library/protocol/CONNEG/library/variants.e create mode 100644 library/protocol/CONNEG/run_test.rb create mode 100644 library/protocol/CONNEG/test/.gitignore create mode 100644 library/protocol/CONNEG/test/application.e create mode 100644 library/protocol/CONNEG/test/common_accept_header_parser_test.e create mode 100644 library/protocol/CONNEG/test/language_parser_test.e create mode 100644 library/protocol/CONNEG/test/mime_parser_test.e create mode 100644 library/protocol/CONNEG/test/test-safe.ecf create mode 100644 library/protocol/CONNEG/test/test.ecf diff --git a/library/protocol/CONNEG/.gitignore b/library/protocol/CONNEG/.gitignore new file mode 100644 index 00000000..a7f9c034 --- /dev/null +++ b/library/protocol/CONNEG/.gitignore @@ -0,0 +1 @@ +EIFGENs/ diff --git a/library/protocol/CONNEG/README.md b/library/protocol/CONNEG/README.md new file mode 100644 index 00000000..c368f653 --- /dev/null +++ b/library/protocol/CONNEG/README.md @@ -0,0 +1,5 @@ +CONNEG is a library that provides utilities to select the best repesentation of a resource for a client +where there are multiple representations available. + +Using this labrary you can retrieve the best Variant for media type, language preference, enconding and compression. +The library is based on eMIME Eiffel MIME library based on Joe Gregorio code diff --git a/library/protocol/CONNEG/library/.gitignore b/library/protocol/CONNEG/library/.gitignore new file mode 100644 index 00000000..ac53b3c2 --- /dev/null +++ b/library/protocol/CONNEG/library/.gitignore @@ -0,0 +1,4 @@ +*~ +EIFGEN* # ignore all files in the EIFGENs/ directory + + diff --git a/library/protocol/CONNEG/library/common_accept_header_parser.e b/library/protocol/CONNEG/library/common_accept_header_parser.e new file mode 100644 index 00000000..f95e6751 --- /dev/null +++ b/library/protocol/CONNEG/library/common_accept_header_parser.e @@ -0,0 +1,282 @@ +note + description: "COMMON_ACCEPT_HEADER_PARSER, this class allows to parse Accept-Charset and Accept-Encoding headers" + author: "" + date: "$Date$" + revision: "$Revision$" + description : "[ + Charset Reference : http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2 + Encoding Reference : http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 + ]" + +class + COMMON_ACCEPT_HEADER_PARSER + + +feature -- Parser + parse_common (header: STRING): COMMON_RESULTS + -- Parses `header' charset/encoding into its component parts. + -- For example, the charset 'iso-8889-5' would get parsed + -- into: + -- ('iso-8889-5', {'q':'1.0'}) + local + l_parts: LIST [STRING] + sub_parts: LIST [STRING] + p: STRING + i: INTEGER + l_header: STRING + do + create Result.make + l_parts := header.split (';') + if l_parts.count = 1 then + Result.put ("1.0", "q") + else + from + i := 1 + until + i > l_parts.count + loop + p := l_parts.at (i) + sub_parts := p.split ('=') + if sub_parts.count = 2 then + Result.put (trim (sub_parts[2]), trim (sub_parts[1])) + end + i := i + 1 + end + end + l_header := trim (l_parts[1]) + Result.set_field (trim (l_header)) + end + + + fitness_and_quality_parsed (a_field: STRING; parsed_charsets: LIST [COMMON_RESULTS]): FITNESS_AND_QUALITY + -- Find the best match for a given charset/encoding against a list of charsets/encodings + -- that have already been parsed by parse_common. Returns a + -- tuple of the fitness value and the value of the 'q' quality parameter of + -- the best match, or (-1, 0) if no match was found. Just as for + -- quality_parsed(). + local + best_fitness: INTEGER + target_q: REAL_64 + best_fit_q: REAL_64 + target: COMMON_RESULTS + range: COMMON_RESULTS + element: detachable STRING + l_fitness: INTEGER + do + best_fitness := -1 + best_fit_q := 0.0 + target := parse_common(a_field) + if attached target.item ("q") as q and then q.is_double then + target_q := q.to_double + if target_q < 0.0 then + target_q := 0.0 + elseif target_q > 1.0 then + target_q := 1.0 + end + else + target_q := 1.0 + end + + if attached target.field as l_target_field + then + from + parsed_charsets.start + until + parsed_charsets.after + loop + range := parsed_charsets.item_for_iteration + if attached range.field as l_range_common then + if l_target_field.same_string (l_range_common) or l_target_field.same_string ("*") or l_range_common.same_string ("*") then + if l_range_common.same_string (l_target_field) then + l_fitness := 100 + else + l_fitness := 0 + end + if l_fitness > best_fitness then + best_fitness := l_fitness + element := range.item ("q") + if element /= Void then + best_fit_q := element.to_double.min (target_q) + else + best_fit_q := 0.0 + end + end + end + end + parsed_charsets.forth + end + end + create Result.make (best_fitness, best_fit_q) + end + + + quality_parsed (a_field: STRING; parsed_common: LIST [COMMON_RESULTS]): REAL_64 + -- Find the best match for a given charset/encoding against a list of charsets/encodings that + -- have already been parsed by parse_charsets(). Returns the 'q' quality + -- parameter of the best match, 0 if no match was found. This function + -- bahaves the same as quality() + do + Result := fitness_and_quality_parsed (a_field, parsed_common).quality + end + + + quality (a_field: STRING; commons: STRING): REAL_64 + -- Returns the quality 'q' of a charset/encoding when compared against the + -- a list of charsets/encodings/ + local + l_commons : LIST [STRING] + res : ARRAYED_LIST [COMMON_RESULTS] + p_res : COMMON_RESULTS + do + l_commons := commons.split (',') + from + create res.make (10); + l_commons.start + until + l_commons.after + loop + p_res := parse_common (l_commons.item_for_iteration) + res.put_left (p_res) + l_commons.forth + end + Result := quality_parsed (a_field, res) + end + + best_match (supported: LIST [STRING]; header: STRING): STRING + -- Choose the accept with the highest fitness score and quality ('q') from a list of candidates. + local + l_header_results: LIST [COMMON_RESULTS] + weighted_matches: LIST [FITNESS_AND_QUALITY] + l_res: LIST [STRING] + p_res: COMMON_RESULTS + fitness_and_quality, first_one: detachable FITNESS_AND_QUALITY + do + l_res := header.split (',') + create {ARRAYED_LIST [COMMON_RESULTS]} l_header_results.make (l_res.count) + + + from + l_res.start + until + l_res.after + loop + p_res := parse_common (l_res.item_for_iteration) + l_header_results.force (p_res) + l_res.forth + end + + create {ARRAYED_LIST [FITNESS_AND_QUALITY]} weighted_matches.make (supported.count) + + from + supported.start + until + supported.after + loop + fitness_and_quality := fitness_and_quality_parsed (supported.item_for_iteration, l_header_results) + fitness_and_quality.set_mime_type (mime_type (supported.item_for_iteration)) + weighted_matches.force (fitness_and_quality) + supported.forth + end + + --| Keep only top quality+fitness types + --| TODO extract method + from + weighted_matches.start + first_one := weighted_matches.item + weighted_matches.forth + until + weighted_matches.after + loop + fitness_and_quality := weighted_matches.item + if first_one < fitness_and_quality then + first_one := fitness_and_quality + if not weighted_matches.isfirst then + from + weighted_matches.back + until + weighted_matches.before + loop + weighted_matches.remove + weighted_matches.back + end + weighted_matches.forth + end + check weighted_matches.item = fitness_and_quality end + weighted_matches.forth + elseif first_one.is_equal (fitness_and_quality) then + weighted_matches.forth + else + check first_one > fitness_and_quality end + weighted_matches.remove + end + end + if first_one /= Void and then first_one.quality /= 0.0 then + if weighted_matches.count = 1 then + Result := first_one.mime_type + else + from + fitness_and_quality := Void + l_header_results.start + until + l_header_results.after or fitness_and_quality /= Void + loop + if attached l_header_results.item.field as l_field then + from + weighted_matches.start + until + weighted_matches.after or fitness_and_quality /= Void + loop + fitness_and_quality := weighted_matches.item + if fitness_and_quality.mime_type.same_string (l_field) then + --| Found + else + fitness_and_quality := Void + weighted_matches.forth + end + end + else + check has_field: False end + end + l_header_results.forth + end + if fitness_and_quality /= Void then + Result := fitness_and_quality.mime_type + else + Result := first_one.mime_type + end + end + else + Result := "" + end + end + +feature -- Util + + mime_type (s: STRING): STRING + local + p: INTEGER + do + p := s.index_of (';', 1) + if p > 0 then + Result := trim (s.substring (1, p - 1)) + else + Result := trim (s.string) + end + end + + trim (a_string: STRING): STRING + -- trim whitespace from the beginning and end of a string + require + valid_argument : a_string /= Void + do + a_string.left_adjust + a_string.right_justify + Result := a_string + ensure + result_same_as_argument: a_string = Result + end + +note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/common_results.e b/library/protocol/CONNEG/library/common_results.e new file mode 100644 index 00000000..5d71a733 --- /dev/null +++ b/library/protocol/CONNEG/library/common_results.e @@ -0,0 +1,119 @@ +note + description: "Summary description for {COMMON_RESULTS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + COMMON_RESULTS +inherit + ANY + redefine + out + end + + DEBUG_OUTPUT + redefine + out + end + +create + make + +feature -- Initialization + + make + do + create params.make (2) + end + +feature -- Access + + field: detachable STRING + + + item (a_key: STRING): detachable STRING + -- Item associated with `a_key', if present + -- otherwise default value of type `STRING' + do + Result := params.item (a_key) + end + + keys: LIST [STRING] + -- arrays of currents keys + local + res: ARRAYED_LIST [STRING] + do + create res.make_from_array (params.current_keys) + Result := res + end + + has_key (a_key: STRING): BOOLEAN + -- Is there an item in the table with key `a_key'? + do + Result := params.has_key (a_key) + end + +feature -- Element change + + set_field (a_field: STRING) + -- Set type with `a_charset' + do + field := a_field + ensure + field_assigned: field ~ field + end + + + put (new: STRING; key: STRING) + -- Insert `new' with `key' if there is no other item + -- associated with the same key. If present, replace + -- the old value with `new' + do + if params.has_key (key) then + params.replace (new, key) + else + params.force (new, key) + end + ensure + has_key: params.has_key (key) + has_item: params.has_item (new) + end + +feature -- Status Report + + out: STRING + -- Representation of the current object + do + create Result.make_from_string ("(") + if attached field as t then + Result.append_string ("'" + t + "',") + end + Result.append_string (" {") + + from + params.start + until + params.after + loop + Result.append ("'" + params.key_for_iteration + "':'" + params.item_for_iteration + "',"); + params.forth + end + Result.append ("})") + end + + debug_output: STRING + -- String that should be displayed in debugger to represent `Current'. + do + Result := out + end + +feature {NONE} -- Implementation + + params: HASH_TABLE [STRING, STRING] + --dictionary of all the parameters for the media range + +;note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/conneg-safe.ecf b/library/protocol/CONNEG/library/conneg-safe.ecf new file mode 100644 index 00000000..cb94e049 --- /dev/null +++ b/library/protocol/CONNEG/library/conneg-safe.ecf @@ -0,0 +1,18 @@ + + + + + + + + + /.git$ + /EIFGENs$ + /CVS$ + /.svn$ + + + + diff --git a/library/protocol/CONNEG/library/conneg.ecf b/library/protocol/CONNEG/library/conneg.ecf new file mode 100644 index 00000000..258bd52d --- /dev/null +++ b/library/protocol/CONNEG/library/conneg.ecf @@ -0,0 +1,19 @@ + + + + + + + + + /EIFGENs$ + /CVS$ + /.svn$ + /.git$ + + + + + diff --git a/library/protocol/CONNEG/library/fitness_and_quality.e b/library/protocol/CONNEG/library/fitness_and_quality.e new file mode 100644 index 00000000..a851ecde --- /dev/null +++ b/library/protocol/CONNEG/library/fitness_and_quality.e @@ -0,0 +1,81 @@ +note + description: "Summary description for {FITNESS_AND_QUALITY}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + FITNESS_AND_QUALITY + +inherit + COMPARABLE + + DEBUG_OUTPUT + undefine + is_equal + end + +create + make + +feature -- Initialization + + make (a_fitness: INTEGER; a_quality: REAL_64) + do + fitness := a_fitness + quality := a_quality + create mime_type.make_empty + ensure + fitness_assigned : fitness = a_fitness + quality_assigned : quality = a_quality + end + +feature -- Access + + fitness: INTEGER + + quality: REAL_64 + + mime_type: STRING + -- optionally used + -- empty by default + + +feature -- Status report + + debug_output: STRING + -- String that should be displayed in debugger to represent `Current'. + do + create Result.make_from_string (mime_type) + Result.append (" (") + Result.append ("quality=" + quality.out) + Result.append (" ; fitness=" + fitness.out) + Result.append (" )") + end + +feature -- Element Change + + set_mime_type (a_mime_type: STRING) + -- set mime_type with `a_mime_type' + do + mime_type := a_mime_type + ensure + mime_type_assigned : mime_type.same_string (a_mime_type) + end + +feature -- Comparision + + is_less alias "<" (other: like Current): BOOLEAN + -- Is current object less than `other'? + do + if fitness = other.fitness then + Result := quality < other.quality + else + Result := fitness < other.fitness + end + end +note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end + diff --git a/library/protocol/CONNEG/library/language_parse.e b/library/protocol/CONNEG/library/language_parse.e new file mode 100644 index 00000000..4373a65a --- /dev/null +++ b/library/protocol/CONNEG/library/language_parse.e @@ -0,0 +1,352 @@ +note + description: "Summary description for {LANGUAGE_PARSE}." + author: "" + date: "$Date$" + revision: "$Revision$" + description : "Language Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4" + +class + LANGUAGE_PARSE +inherit + REFACTORING_HELPER + +feature -- Parser + + parse_mime_type (a_mime_type: STRING): LANGUAGE_RESULTS + -- Parses a mime-type into its component parts. + -- For example, the media range 'application/xhtml;q=0.5' would get parsed + -- into: + -- ('application', 'xhtml', {'q', '0.5'}) + local + l_parts: LIST [STRING] + p: STRING + sub_parts: LIST [STRING] + i: INTEGER + l_full_type: STRING + l_types: LIST [STRING] + do + fixme ("Improve code!!!") + create Result.make + l_parts := a_mime_type.split (';') + from + i := 1 + until + i > l_parts.count + loop + p := l_parts.at (i) + sub_parts := p.split ('=') + if sub_parts.count = 2 then + Result.put (trim (sub_parts[2]), trim (sub_parts[1])) + end + i := i + 1 + end + --Java URLConnection class sends an Accept header that includes a + --single "*" - Turn it into a legal wildcard. + + l_full_type := trim (l_parts[1]) + if l_full_type.same_string ("*") then + l_full_type := "*" + end + l_types := l_full_type.split ('-') + if l_types.count = 1 then + Result.set_type (trim (l_types[1])) + else + Result.set_type (trim (l_types[1])) + Result.set_sub_type (trim (l_types[2])) + end + end + + parse_media_range (a_range: STRING): LANGUAGE_RESULTS + -- Media-ranges are mime-types with wild-cards and a 'q' quality parameter. + -- For example, the media range 'application/*;q=0.5' would get parsed into: + -- ('application', '*', {'q', '0.5'}) + -- In addition this function also guarantees that there is a value for 'q' + -- in the params dictionary, filling it in with a proper default if + -- necessary. + do + fixme ("Improve the code!!!") + Result := parse_mime_type (a_range) + if attached Result.item ("q") as q then + if + q.is_double and then + attached {REAL_64} q.to_double as r and then + (r >= 0.0 and r <= 1.0) + then + --| Keep current value + if q.same_string ("1") then + --| Use 1.0 formatting + Result.put ("1.0", "q") + end + else + Result.put ("1.0", "q") + end + else + Result.put ("1.0", "q") + end + end + + + fitness_and_quality_parsed (a_mime_type: STRING; parsed_ranges: LIST [LANGUAGE_RESULTS]): FITNESS_AND_QUALITY + -- Find the best match for a given mimeType against a list of media_ranges + -- that have already been parsed by parse_media_range. Returns a + -- tuple of the fitness value and the value of the 'q' quality parameter of + -- the best match, or (-1, 0) if no match was found. Just as for + -- quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges. + local + best_fitness: INTEGER + target_q: REAL_64 + best_fit_q: REAL_64 + target: LANGUAGE_RESULTS + range: LANGUAGE_RESULTS + keys: LIST [STRING] + param_matches: INTEGER + element: detachable STRING + l_fitness: INTEGER + do + best_fitness := -1 + best_fit_q := 0.0 + target := parse_media_range (a_mime_type) + if attached target.item ("q") as q and then q.is_double then + target_q := q.to_double + if target_q < 0.0 then + target_q := 0.0 + elseif target_q > 1.0 then + target_q := 1.0 + end + else + target_q := 1.0 + end + + if + attached target.type as l_target_type + then + from + parsed_ranges.start + until + parsed_ranges.after + loop + range := parsed_ranges.item_for_iteration + if + ( + attached range.type as l_range_type and then + (l_target_type.same_string (l_range_type) or l_range_type.same_string ("*") or l_target_type.same_string ("*")) + ) + then + from + param_matches := 0 + keys := target.keys + keys.start + until + keys.after + loop + element := keys.item_for_iteration + if + not element.same_string ("q") and then + range.has_key (element) and then + (attached target.item (element) as t_item and attached range.item (element) as r_item) and then + t_item.same_string (r_item) + then + param_matches := param_matches + 1 + end + keys.forth + end + + if l_range_type.same_string (l_target_type) then + l_fitness := 100 + else + l_fitness := 0 + end + if ( + attached range.sub_type as l_range_sub_type and then attached target.sub_type as l_target_sub_type and then + (l_target_sub_type.same_string (l_range_sub_type) or l_range_sub_type.same_string ("*") or l_target_sub_type.same_string ("*")) + ) then + if l_range_sub_type.same_string (l_target_sub_type) then + l_fitness := l_fitness + 10 + end + end + + l_fitness := l_fitness + param_matches + + if l_fitness > best_fitness then + best_fitness := l_fitness + element := range.item ("q") + if element /= Void then + best_fit_q := element.to_double.min (target_q) + else + best_fit_q := 0.0 + end + end + end + parsed_ranges.forth + end + end + create Result.make (best_fitness, best_fit_q) + end + + quality_parsed (a_mime_type: STRING; parsed_ranges: LIST [LANGUAGE_RESULTS]): REAL_64 + -- Find the best match for a given mime-type against a list of ranges that + -- have already been parsed by parseMediaRange(). Returns the 'q' quality + -- parameter of the best match, 0 if no match was found. This function + -- bahaves the same as quality() except that 'parsed_ranges' must be a list + -- of parsed media ranges. + do + Result := fitness_and_quality_parsed (a_mime_type, parsed_ranges).quality + end + + quality (a_mime_type: STRING; ranges: STRING): REAL_64 + -- Returns the quality 'q' of a mime-type when compared against the + -- mediaRanges in ranges. + local + l_ranges : LIST [STRING] + res : ARRAYED_LIST [LANGUAGE_RESULTS] + p_res : LANGUAGE_RESULTS + do + l_ranges := ranges.split (',') + from + create res.make (10); + l_ranges.start + until + l_ranges.after + loop + p_res := parse_media_range (l_ranges.item_for_iteration) + res.put_left (p_res) + l_ranges.forth + end + Result := quality_parsed (a_mime_type, res) + end + + best_match (supported: LIST [STRING]; header: STRING): STRING + -- Choose the mime-type with the highest fitness score and quality ('q') from a list of candidates. + local + l_header_results: LIST [LANGUAGE_RESULTS] + weighted_matches: LIST [FITNESS_AND_QUALITY] + l_res: LIST [STRING] + p_res: LANGUAGE_RESULTS + fitness_and_quality, first_one: detachable FITNESS_AND_QUALITY + s: STRING + do + l_res := header.split (',') + create {ARRAYED_LIST [LANGUAGE_RESULTS]} l_header_results.make (l_res.count) + + fixme("Extract method!!!") + from + l_res.start + until + l_res.after + loop + p_res := parse_media_range (l_res.item_for_iteration) + l_header_results.force (p_res) + l_res.forth + end + + create {ARRAYED_LIST [FITNESS_AND_QUALITY]} weighted_matches.make (supported.count) + + from + supported.start + until + supported.after + loop + fitness_and_quality := fitness_and_quality_parsed (supported.item_for_iteration, l_header_results) + fitness_and_quality.set_mime_type (mime_type (supported.item_for_iteration)) + weighted_matches.force (fitness_and_quality) + supported.forth + end + + --| Keep only top quality+fitness types + from + weighted_matches.start + first_one := weighted_matches.item + weighted_matches.forth + until + weighted_matches.after + loop + fitness_and_quality := weighted_matches.item + if first_one < fitness_and_quality then + first_one := fitness_and_quality + if not weighted_matches.isfirst then + from + weighted_matches.back + until + weighted_matches.before + loop + weighted_matches.remove + weighted_matches.back + end + weighted_matches.forth + end + check weighted_matches.item = fitness_and_quality end + weighted_matches.forth + elseif first_one.is_equal (fitness_and_quality) then + weighted_matches.forth + else + check first_one > fitness_and_quality end + weighted_matches.remove + end + end + if first_one /= Void and then first_one.quality /= 0.0 then + if weighted_matches.count = 1 then + Result := first_one.mime_type + else + from + fitness_and_quality := Void + l_header_results.start + until + l_header_results.after or fitness_and_quality /= Void + loop + s := l_header_results.item.mime_type + from + weighted_matches.start + until + weighted_matches.after or fitness_and_quality /= Void + loop + fitness_and_quality := weighted_matches.item + if fitness_and_quality.mime_type.same_string (s) then + --| Found + else + fitness_and_quality := Void + weighted_matches.forth + end + end + l_header_results.forth + end + if fitness_and_quality /= Void then + Result := fitness_and_quality.mime_type + else + Result := first_one.mime_type + end + end + else + Result := "" + end + end + +feature {NONE} -- Implementation + + mime_type (s: STRING): STRING + local + p: INTEGER + do + p := s.index_of (';', 1) + if p > 0 then + Result := trim (s.substring (1, p - 1)) + else + Result := trim (s.string) + end + end + + trim (a_string: STRING): STRING + -- trim whitespace from the beginning and end of a string + require + valid_argument : a_string /= Void + do + a_string.left_adjust + a_string.right_justify + Result := a_string + ensure + result_same_as_argument: a_string = Result + end + +note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/language_results.e b/library/protocol/CONNEG/library/language_results.e new file mode 100644 index 00000000..fe8d0aa0 --- /dev/null +++ b/library/protocol/CONNEG/library/language_results.e @@ -0,0 +1,143 @@ +note + description: "Summary description for {LANGUAGE_RESULTS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + LANGUAGE_RESULTS +inherit + ANY + redefine + out + end + + DEBUG_OUTPUT + redefine + out + end + +create + make + +feature -- Initialization + + make + do + create params.make (2) + create mime_type.make_from_string ("*") + end + +feature -- Access + + type: detachable STRING + + sub_type: detachable STRING + + mime_type: STRING + + item (a_key: STRING): detachable STRING + -- Item associated with `a_key', if present + -- otherwise default value of type `STRING' + do + Result := params.item (a_key) + end + + keys: LIST [STRING] + -- arrays of currents keys + local + res: ARRAYED_LIST [STRING] + do + create res.make_from_array (params.current_keys) + Result := res + end + + has_key (a_key: STRING): BOOLEAN + -- Is there an item in the table with key `a_key'? + do + Result := params.has_key (a_key) + end + +feature -- Element change + + set_type (a_type: STRING) + -- Set type with `a_type' + do + type := a_type + if attached sub_type as st then + mime_type := a_type + "-" + st + else + mime_type := a_type + end + ensure + type_assigned: type ~ a_type + end + + set_sub_type (a_sub_type: STRING) + -- Set sub_type with `a_sub_type + do + sub_type := a_sub_type + if attached type as t then + mime_type := t + "-" + a_sub_type + else + mime_type := "*" + end + ensure + sub_type_assigned: sub_type ~ a_sub_type + end + + put (new: STRING; key: STRING) + -- Insert `new' with `key' if there is no other item + -- associated with the same key. If present, replace + -- the old value with `new' + do + if params.has_key (key) then + params.replace (new, key) + else + params.force (new, key) + end + ensure + has_key: params.has_key (key) + has_item: params.has_item (new) + end + +feature -- Status Report + + out: STRING + -- Representation of the current object + do + create Result.make_from_string ("(") + if attached type as t then + Result.append_string ("'" + t + "',") + end + if attached sub_type as st then + Result.append_string (" '" + st + "',") + end + Result.append_string (" {") + + from + params.start + until + params.after + loop + Result.append ("'" + params.key_for_iteration + "':'" + params.item_for_iteration + "',"); + params.forth + end + Result.append ("})") + end + + debug_output: STRING + -- String that should be displayed in debugger to represent `Current'. + do + Result := out + end + +feature {NONE} -- Implementation + + params: HASH_TABLE [STRING, STRING] + --dictionary of all the parameters for the media range + +;note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/license.lic b/library/protocol/CONNEG/library/license.lic new file mode 100644 index 00000000..42cd9b4e --- /dev/null +++ b/library/protocol/CONNEG/library/license.lic @@ -0,0 +1,4 @@ +${NOTE_KEYWORD} + copyright: "2011-${YEAR}, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + diff --git a/library/protocol/CONNEG/library/mime_parse.e b/library/protocol/CONNEG/library/mime_parse.e new file mode 100644 index 00000000..9d92ba36 --- /dev/null +++ b/library/protocol/CONNEG/library/mime_parse.e @@ -0,0 +1,349 @@ +note + description: "Summary description for {MIME_PARSE}." + author: "" + date: "$Date$" + revision: "$Revision$" + description : "Accept Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1" +class + MIME_PARSE + +inherit + REFACTORING_HELPER + +feature -- Parser + + parse_mime_type (a_mime_type: STRING): PARSE_RESULTS + -- Parses a mime-type into its component parts. + -- For example, the media range 'application/xhtml;q=0.5' would get parsed + -- into: + -- ('application', 'xhtml', {'q', '0.5'}) + local + l_parts: LIST [STRING] + p: STRING + sub_parts: LIST [STRING] + i: INTEGER + l_full_type: STRING + l_types: LIST [STRING] + do + fixme ("Improve code!!!") + create Result.make + l_parts := a_mime_type.split (';') + from + i := 1 + until + i > l_parts.count + loop + p := l_parts.at (i) + sub_parts := p.split ('=') + if sub_parts.count = 2 then + Result.put (trim (sub_parts[2]), trim (sub_parts[1])) + end + i := i + 1 + end + --Java URLConnection class sends an Accept header that includes a + --single "*" - Turn it into a legal wildcard. + + l_full_type := trim (l_parts[1]) + if l_full_type.same_string ("*") then + l_full_type := "*/*" + end + l_types := l_full_type.split ('/') + Result.set_type (trim (l_types[1])) + Result.set_sub_type (trim (l_types[2])) + end + + parse_media_range (a_range: STRING): PARSE_RESULTS + -- Media-ranges are mime-types with wild-cards and a 'q' quality parameter. + -- For example, the media range 'application/*;q=0.5' would get parsed into: + -- ('application', '*', {'q', '0.5'}) + -- In addition this function also guarantees that there is a value for 'q' + -- in the params dictionary, filling it in with a proper default if + -- necessary. + do + fixme ("Improve the code!!!") + Result := parse_mime_type (a_range) + if attached Result.item ("q") as q then + if + q.is_double and then + attached {REAL_64} q.to_double as r and then + (r >= 0.0 and r <= 1.0) + then + --| Keep current value + if q.same_string ("1") then + --| Use 1.0 formatting + Result.put ("1.0", "q") + end + else + Result.put ("1.0", "q") + end + else + Result.put ("1.0", "q") + end + end + + + fitness_and_quality_parsed (a_mime_type: STRING; parsed_ranges: LIST [PARSE_RESULTS]): FITNESS_AND_QUALITY + -- Find the best match for a given mimeType against a list of media_ranges + -- that have already been parsed by parse_media_range. Returns a + -- tuple of the fitness value and the value of the 'q' quality parameter of + -- the best match, or (-1, 0) if no match was found. Just as for + -- quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges. + local + best_fitness: INTEGER + target_q: REAL_64 + best_fit_q: REAL_64 + target: PARSE_RESULTS + range: PARSE_RESULTS + keys: LIST [STRING] + param_matches: INTEGER + element: detachable STRING + l_fitness: INTEGER + do + best_fitness := -1 + best_fit_q := 0.0 + target := parse_media_range (a_mime_type) + if attached target.item ("q") as q and then q.is_double then + target_q := q.to_double + if target_q < 0.0 then + target_q := 0.0 + elseif target_q > 1.0 then + target_q := 1.0 + end + else + target_q := 1.0 + end + + if + attached target.type as l_target_type and + attached target.sub_type as l_target_sub_type + then + from + parsed_ranges.start + until + parsed_ranges.after + loop + range := parsed_ranges.item_for_iteration + if + ( + attached range.type as l_range_type and then + (l_target_type.same_string (l_range_type) or l_range_type.same_string ("*") or l_target_type.same_string ("*")) + ) and + ( + attached range.sub_type as l_range_sub_type and then + (l_target_sub_type.same_string (l_range_sub_type) or l_range_sub_type.same_string ("*") or l_target_sub_type.same_string ("*")) + ) + then + from + param_matches := 0 + keys := target.keys + keys.start + until + keys.after + loop + element := keys.item_for_iteration + if + not element.same_string ("q") and then + range.has_key (element) and then + (attached target.item (element) as t_item and attached range.item (element) as r_item) and then + t_item.same_string (r_item) + then + param_matches := param_matches + 1 + end + keys.forth + end + + if l_range_type.same_string (l_target_type) then + l_fitness := 100 + else + l_fitness := 0 + end + + if l_range_sub_type.same_string (l_target_sub_type) then + l_fitness := l_fitness + 10 + end + + l_fitness := l_fitness + param_matches + + if l_fitness > best_fitness then + best_fitness := l_fitness + element := range.item ("q") + if element /= Void then + best_fit_q := element.to_double.min (target_q) + else + best_fit_q := 0.0 + end + end + end + parsed_ranges.forth + end + end + create Result.make (best_fitness, best_fit_q) + end + + quality_parsed (a_mime_type: STRING; parsed_ranges: LIST [PARSE_RESULTS]): REAL_64 + -- Find the best match for a given mime-type against a list of ranges that + -- have already been parsed by parseMediaRange(). Returns the 'q' quality + -- parameter of the best match, 0 if no match was found. This function + -- bahaves the same as quality() except that 'parsed_ranges' must be a list + -- of parsed media ranges. + do + Result := fitness_and_quality_parsed (a_mime_type, parsed_ranges).quality + end + + quality (a_mime_type: STRING; ranges: STRING): REAL_64 + -- Returns the quality 'q' of a mime-type when compared against the + -- mediaRanges in ranges. + local + l_ranges : LIST [STRING] + res : ARRAYED_LIST [PARSE_RESULTS] + p_res : PARSE_RESULTS + do + l_ranges := ranges.split (',') + from + create res.make (10); + l_ranges.start + until + l_ranges.after + loop + p_res := parse_media_range (l_ranges.item_for_iteration) + res.put_left (p_res) + l_ranges.forth + end + Result := quality_parsed (a_mime_type, res) + end + + best_match (supported: LIST [STRING]; header: STRING): STRING + -- Choose the mime-type with the highest fitness score and quality ('q') from a list of candidates. + local + l_header_results: LIST [PARSE_RESULTS] + weighted_matches: LIST [FITNESS_AND_QUALITY] + l_res: LIST [STRING] + p_res: PARSE_RESULTS + fitness_and_quality, first_one: detachable FITNESS_AND_QUALITY + s: STRING + do + l_res := header.split (',') + create {ARRAYED_LIST [PARSE_RESULTS]} l_header_results.make (l_res.count) + + fixme("Extract method!!!") + from + l_res.start + until + l_res.after + loop + p_res := parse_media_range (l_res.item_for_iteration) + l_header_results.force (p_res) + l_res.forth + end + + create {ARRAYED_LIST [FITNESS_AND_QUALITY]} weighted_matches.make (supported.count) + + from + supported.start + until + supported.after + loop + fitness_and_quality := fitness_and_quality_parsed (supported.item_for_iteration, l_header_results) + fitness_and_quality.set_mime_type (mime_type (supported.item_for_iteration)) + weighted_matches.force (fitness_and_quality) + supported.forth + end + + --| Keep only top quality+fitness types + from + weighted_matches.start + first_one := weighted_matches.item + weighted_matches.forth + until + weighted_matches.after + loop + fitness_and_quality := weighted_matches.item + if first_one < fitness_and_quality then + first_one := fitness_and_quality + if not weighted_matches.isfirst then + from + weighted_matches.back + until + weighted_matches.before + loop + weighted_matches.remove + weighted_matches.back + end + weighted_matches.forth + end + check weighted_matches.item = fitness_and_quality end + weighted_matches.forth + elseif first_one.is_equal (fitness_and_quality) then + weighted_matches.forth + else + check first_one > fitness_and_quality end + weighted_matches.remove + end + end + if first_one /= Void and then first_one.quality /= 0.0 then + if weighted_matches.count = 1 then + Result := first_one.mime_type + else + from + fitness_and_quality := Void + l_header_results.start + until + l_header_results.after or fitness_and_quality /= Void + loop + s := l_header_results.item.mime_type + from + weighted_matches.start + until + weighted_matches.after or fitness_and_quality /= Void + loop + fitness_and_quality := weighted_matches.item + if fitness_and_quality.mime_type.same_string (s) then + --| Found + else + fitness_and_quality := Void + weighted_matches.forth + end + end + l_header_results.forth + end + if fitness_and_quality /= Void then + Result := fitness_and_quality.mime_type + else + Result := first_one.mime_type + end + end + else + Result := "" + end + end + +feature {NONE} -- Implementation + + mime_type (s: STRING): STRING + local + p: INTEGER + do + p := s.index_of (';', 1) + if p > 0 then + Result := trim (s.substring (1, p - 1)) + else + Result := trim (s.string) + end + end + + trim (a_string: STRING): STRING + -- trim whitespace from the beginning and end of a string + require + valid_argument : a_string /= Void + do + a_string.left_adjust + a_string.right_justify + Result := a_string + ensure + result_same_as_argument: a_string = Result + end + +note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/parse_results.e b/library/protocol/CONNEG/library/parse_results.e new file mode 100644 index 00000000..185995ea --- /dev/null +++ b/library/protocol/CONNEG/library/parse_results.e @@ -0,0 +1,144 @@ +note + description: "Summary description for {PARSE_RESULTS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + PARSE_RESULTS + +inherit + ANY + redefine + out + end + + DEBUG_OUTPUT + redefine + out + end + +create + make + +feature -- Initialization + + make + do + create params.make (2) + create mime_type.make_from_string ("*/*") + end + +feature -- Access + + type: detachable STRING + + sub_type: detachable STRING + + mime_type: STRING + + item (a_key: STRING): detachable STRING + -- Item associated with `a_key', if present + -- otherwise default value of type `STRING' + do + Result := params.item (a_key) + end + + keys: LIST [STRING] + -- arrays of currents keys + local + res: ARRAYED_LIST [STRING] + do + create res.make_from_array (params.current_keys) + Result := res + end + + has_key (a_key: STRING): BOOLEAN + -- Is there an item in the table with key `a_key'? + do + Result := params.has_key (a_key) + end + +feature -- Element change + + set_type (a_type: STRING) + -- Set type with `a_type' + do + type := a_type + if attached sub_type as st then + mime_type := a_type + "/" + st + else + mime_type := a_type + "/*" + end + ensure + type_assigned: type ~ a_type + end + + set_sub_type (a_sub_type: STRING) + -- Set sub_type with `a_sub_type + do + sub_type := a_sub_type + if attached type as t then + mime_type := t + "/" + a_sub_type + else + mime_type := "*/" + a_sub_type + end + ensure + sub_type_assigned: sub_type ~ a_sub_type + end + + put (new: STRING; key: STRING) + -- Insert `new' with `key' if there is no other item + -- associated with the same key. If present, replace + -- the old value with `new' + do + if params.has_key (key) then + params.replace (new, key) + else + params.force (new, key) + end + ensure + has_key: params.has_key (key) + has_item: params.has_item (new) + end + +feature -- Status Report + + out: STRING + -- Representation of the current object + do + create Result.make_from_string ("(") + if attached type as t then + Result.append_string ("'" + t + "',") + end + if attached sub_type as st then + Result.append_string (" '" + st + "',") + end + Result.append_string (" {") + + from + params.start + until + params.after + loop + Result.append ("'" + params.key_for_iteration + "':'" + params.item_for_iteration + "',"); + params.forth + end + Result.append ("})") + end + + debug_output: STRING + -- String that should be displayed in debugger to represent `Current'. + do + Result := out + end + +feature {NONE} -- Implementation + + params: HASH_TABLE [STRING, STRING] + --dictionary of all the parameters for the media range + +;note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/shared_conneg.e b/library/protocol/CONNEG/library/shared_conneg.e new file mode 100644 index 00000000..ffa6a47f --- /dev/null +++ b/library/protocol/CONNEG/library/shared_conneg.e @@ -0,0 +1,30 @@ +note + description: "Summary description for {SHARED_MIME}." + date: "$Date$" + revision: "$Revision$" + +class + SHARED_CONNEG + +feature + + mime: MIME_PARSE + once + create Result + end + + common: COMMON_ACCEPT_HEADER_PARSER + -- Charset and Encoding + once + create Result + end + + language: LANGUAGE_PARSE + once + create Result + end + +note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/protocol/CONNEG/library/variants.e b/library/protocol/CONNEG/library/variants.e new file mode 100644 index 00000000..3379edc9 --- /dev/null +++ b/library/protocol/CONNEG/library/variants.e @@ -0,0 +1,76 @@ +note + description: "Summary description for {VARIANTS}. Utility class to support Server Side Content Negotiation " + author: "" + date: "$Date$" + revision: "$Revision$" + description: "[ + Reference : http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html#sec12.1 + Server-driven Negotiation : If the selection of the best representation for a response is made by an algorithm located at the server, + it is called server-driven negotiation. Selection is based on the available representations of the response (the dimensions over which it can vary; e.g. language, content-coding, etc.) + and the contents of particular header fields in the request message or on other information pertaining to the request (such as the network address of the client). + Server-driven negotiation is advantageous when the algorithm for selecting from among the available representations is difficult to describe to the user agent, + or when the server desires to send its "best guess" to the client along with the first response (hoping to avoid the round-trip delay of a subsequent request if the "best guess" is good enough for the user). + In order to improve the server's guess, the user agent MAY include request header fields (Accept, Accept-Language, Accept-Encoding, etc.) which describe its preferences for such a response. +]" +class + VARIANTS + +inherit + SHARED_CONNEG + REFACTORING_HELPER +feature -- Media Type Negotiation + + media_type_preference ( mime_types_supported : LIST[STRING]; header : STRING) : STRING + -- mime_types_supported represent media types supported by the server. + -- header represent the Accept header, ie, the client preferences. + -- Return which media type to use for representaion in a response, if the server support + -- one media type, or empty in other case. + -- Reference : http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + do + Result := mime.best_match (mime_types_supported, header) + end + + +feature -- Encoding Negotiation + + charset_preference (server_charset_supported : LIST[STRING]; header: STRING) : STRING + -- server_charset_supported represent a list of charset supported by the server. + -- header represent the Accept-Charset header, ie, the client preferences. + -- Return which Charset to use in a response, if the server support + -- one Charset, or empty in other case. + -- Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2 + do + Result := common.best_match (server_charset_supported, header) + end + + +feature -- Compression Negotiation + + encoding_preference (server_encoding_supported : LIST[STRING]; header: STRING) : STRING + -- server_encoding_supported represent a list of encoding supported by the server. + -- header represent the Accept-Encoding header, ie, the client preferences. + -- Return which Encoding to use in a response, if the server support + -- one Encoding, or empty in other case. + -- Representation: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 + do + Result := common.best_match (server_encoding_supported, header) + end + +feature -- Language Negotiation + + language_preference (server_language_supported : LIST[STRING]; header: STRING) : STRING + -- server_language_supported represent a list of languages supported by the server. + -- header represent the Accept-Language header, ie, the client preferences. + -- Return which Language to use in a response, if the server support + -- one Language, or empty in other case. + -- Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 + do + Result := language.best_match (server_language_supported, header) + end + +note + copyright: "2011-2011, Javier Velilla, Jocelyn Fiat and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end + + diff --git a/library/protocol/CONNEG/run_test.rb b/library/protocol/CONNEG/run_test.rb new file mode 100644 index 00000000..4ee95c80 --- /dev/null +++ b/library/protocol/CONNEG/run_test.rb @@ -0,0 +1,79 @@ +#!/usr/bin/env ruby +# Niklaus Giger, 15.01.2011 +# Small ruby-script run all tests using ec (the Eiffel compiler) +# we assumen that ec outputs everything in english! + +# For the command line options look at +# http://docs.eiffel.com/book/eiffelstudio/eiffelstudio-command-line-options +# we use often the -batch open. +# +# TODO: Fix problems when compiling takes too long and/or there +# are ec process lingering around from a previous failed build + +require 'tempfile' +require 'fileutils' + +# Override system command. +# run command. if not successful, complain and exit with error +def system(cmd) + puts cmd + res = Kernel.system(cmd) + if !res + puts "Failed running: #{cmd}" + exit 2 + end +end + + +def runTestForProject(where) + if !File.directory?(where) + puts "Directory #{where} does not exist" + exit 2 + end + + # create a temporary file with input for the + # interactive mode of ec + commands2run=< + + + + + + + + + + /EIFGENs$ + /CVS$ + /.svn$ + /.git$ + + + + diff --git a/library/protocol/CONNEG/test/test.ecf b/library/protocol/CONNEG/test/test.ecf new file mode 100644 index 00000000..dbacffdd --- /dev/null +++ b/library/protocol/CONNEG/test/test.ecf @@ -0,0 +1,21 @@ + + + + + + + + + + + + /.git$ + /EIFGENs$ + /CVS$ + /.svn$ + + + +