From 8dbd24afd1dd7a75b71ce09e7661b6390775f856 Mon Sep 17 00:00:00 2001 From: Colin Adams Date: Tue, 6 Aug 2013 13:57:12 +0100 Subject: [PATCH] Policy-driven URI template handlers --- .../server/wsf/router/wsf_caching_policy.e | 125 ++++++ library/server/wsf/router/wsf_delete_helper.e | 60 +++ library/server/wsf/router/wsf_method_helper.e | 375 +++++++++++++++++- .../wsf/router/wsf_method_helper_factory.e | 8 +- library/server/wsf/router/wsf_post_helper.e | 70 ++++ .../server/wsf/router/wsf_previous_policy.e | 30 +- library/server/wsf/router/wsf_put_helper.e | 75 ++++ .../server/wsf/router/wsf_skeleton_handler.e | 367 ++++++++++++++++- 8 files changed, 1084 insertions(+), 26 deletions(-) create mode 100644 library/server/wsf/router/wsf_caching_policy.e create mode 100644 library/server/wsf/router/wsf_delete_helper.e create mode 100644 library/server/wsf/router/wsf_post_helper.e create mode 100644 library/server/wsf/router/wsf_put_helper.e diff --git a/library/server/wsf/router/wsf_caching_policy.e b/library/server/wsf/router/wsf_caching_policy.e new file mode 100644 index 00000000..4c780b19 --- /dev/null +++ b/library/server/wsf/router/wsf_caching_policy.e @@ -0,0 +1,125 @@ +note + + description: "[ + Policies for determing caching of responses. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class WSF_CACHING_POLICY + +feature -- Access + + age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale; + -- This is used to generate a Cache-Control: max-age header. + -- Return 0 to indicate already expired. + -- Return (365 * 1440 = 1 year) to indicate never expires. + require + req_attached: req /= Void + deferred + ensure + not_more_than_1_year: Result <= (365 * 1440).as_natural_32 + end + + shared_age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale in a shared cache; + -- This is used to generate a Cache-Control: s-maxage header. + -- If you wish to have different expiry ages for shared and provate caches, redefine this routine. + -- Return 0 to indicate already expired. + -- Return (365 * 1440 = 1 year) to indicate never expires. + require + req_attached: req /= Void + do + Result := age (req) + ensure + not_more_than_1_year: Result <= (365 * 1440).as_natural_32 + end + + http_1_0_age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale; + -- This is used to generate an Expires header, which HTTP/1.0 caches understand. + -- If you wish to generate a different age for HTTP/1.0 caches, then redefine this routine. + -- Return 0 to indicate already expired. + -- Return (365 * 1440 = 1 year) to indicate never expires. Note this will + -- make a result cachecable that would not normally be cacheable (such as as response + -- to a POST), unless overriden by cache-control headers, so be sure to check `req.request_method'. + require + req_attached: req /= Void + do + Result := age (req) + ensure + not_more_than_1_year: Result <= (365 * 1440).as_natural_32 + end + + is_freely_cacheable (req: WSF_REQUEST): BOOLEAN + -- Should the response to `req' be freely cachable in shared caches? + -- If `True', then a Cache-Control: public header will be generated. + require + req_attached: req /= Void + deferred + end + + is_transformable (req: WSF_REQUEST): BOOLEAN + -- Should a non-transparent proxy be allowed to modify headers of response to `req`? + -- The headers concerned are listed in http://www.w3.org/Protocols/rfc2616-sec14.html#sec14,9. + -- If `False' then a Cache-Control: no-transorm header will be generated. + require + req_attached: req /= Void + do + -- We choose a conservative default. But most applications can + -- redefine to return `True'. + end + + must_revalidate (req: WSF_REQUEST): BOOLEAN + -- If a client has requested, or a cache is configured, to ignore server's expiration time, + -- should we force revalidation anyway? + -- If `True' then a Cache-Control: must-revalidate header will be generated. + require + req_attached: req /= Void + do + -- Redefine to force revalidation. + end + + + must_proxy_revalidate (req: WSF_REQUEST): BOOLEAN + -- If a shared cache is configured to ignore server's expiration time, + -- should we force revalidation anyway? + -- If `True' then a Cache-Control: proxy-revalidate header will be generated. + require + req_attached: req /= Void + do + -- Redefine to force revalidation. + end + + + private_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8] + -- Header names intended for a single user. + -- If non-Void, then a Cache-Control: private header will be generated. + -- Returning an empty list prevents the entire response from being served from a shared cache. + require + req_attached: req /= Void + deferred + ensure + not_freely_cacheable: Result /= Void implies not is_freely_cacheable (req) + end + + non_cacheable_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8] + -- Header names that will not be sent from a cache without revalidation; + -- If non-Void, then a Cache-Control: no-cache header will be generated. + -- Returning an empty list prevents the response being served from a cache + -- without revalidation. + require + req_attached: req /= Void + deferred + end + + is_sensitive (req: WSF_REQUEST): BOOLEAN + -- Is the response to `req' of a sensitive nature? + -- If `True' then a Cache-Control: no-store header will be generated. + require + req_attached: req /= Void + deferred + end + +end diff --git a/library/server/wsf/router/wsf_delete_helper.e b/library/server/wsf/router/wsf_delete_helper.e new file mode 100644 index 00000000..6bc0e646 --- /dev/null +++ b/library/server/wsf/router/wsf_delete_helper.e @@ -0,0 +1,60 @@ +note + + description: "[ + Policy-driven helpers to implement the DELETE method. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_DELETE_HELPER + +inherit + + WSF_METHOD_HELPER + rename + send_get_response as handle_delete + redefine + handle_delete + end + +feature {NONE} -- Implementation + + handle_delete (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; + a_media_type, a_language_type, a_character_type, a_compression_type: detachable READABLE_STRING_8; a_new_resource: BOOLEAN) + -- Write response to deletion of resource named by `req' into `res' in accordance with `a_media_type' etc. + local + l_dt: STRING + do + a_handler.delete (req) + if a_handler.includes_response_entity (req) then + a_handler.ensure_content_available (req, a_media_type, a_language_type, a_character_type, a_compression_type) + a_header.put_content_length (a_handler.content_length (req).as_integer_32) + -- we don't bother supporting chunked responses for DELETE. + else + a_header.put_content_length (0) + end + if attached req.request_time as l_time then + l_dt := (create {HTTP_DATE}.make_from_date_time (l_time)).rfc1123_string + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_date, l_dt) + generate_cache_headers (req, a_handler, a_header, l_time) + end + if a_handler.delete_queued (req) then + res.set_status_code ({HTTP_STATUS_CODE}.accepted) + res.put_header_text (a_header.string) + res.put_string (a_handler.content (req, a_media_type, a_language_type, a_character_type, a_compression_type)) + elseif a_handler.deleted (req) then + if a_handler.includes_response_entity (req) then + res.set_status_code ({HTTP_STATUS_CODE}.ok) + res.put_header_text (a_header.string) + res.put_string (a_handler.content (req, a_media_type, a_language_type, a_character_type, a_compression_type)) + else + res.set_status_code ({HTTP_STATUS_CODE}.no_content) + res.put_header_text (a_header.string) + end + else + -- TODO - req.error_handler.has_error = True + --handle_internal_server_error (a_handler.last_error (req), req, res) + end + end + +end diff --git a/library/server/wsf/router/wsf_method_helper.e b/library/server/wsf/router/wsf_method_helper.e index 135876c5..6b23ebec 100644 --- a/library/server/wsf/router/wsf_method_helper.e +++ b/library/server/wsf/router/wsf_method_helper.e @@ -2,11 +2,12 @@ note description: "[ Policy-driven helpers to implement a method. - ]" + This implementation is suitable for GET and HEAD. + ]" date: "$Date$" revision: "$Revision$" -deferred class WSF_METHOD_HELPER +class WSF_METHOD_HELPER feature -- Access @@ -34,24 +35,37 @@ feature -- Basic operations req_attached: req /= Void res_attached: res /= Void a_handler_attached: a_handler /= Void + local + l_locs: LIST [URI] + h: HTTP_HEADER do if a_handler.resource_previously_existed (req) then - --| TODO - should we be passing the entire request, or should we further - --| simplify the programmer's task by passing `req.path_translated'? if a_handler.resource_moved_permanently (req) then - -- TODO 301 Moved Permanently + l_locs := a_handler.previous_location (req) + -- TODO 301 Moved permanently response elseif a_handler.resource_moved_temporarily (req) then - -- TODO 302 Found + l_locs := a_handler.previous_location (req) + -- TODO := 302 Found response else - -- TODO 410 Gone + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.gone) + res.put_header_text (h.string) end else - -- TODO 404 Not Found + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.not_found) + res.put_header_text (h.string) end end - - execute_existing_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) - -- Write response to existing resource requested by `req.' into `res'. + + execute_existing_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to existing resource requested by `req' into `res'. -- Policy routines are available in `a_handler'. require req_attached: req /= Void @@ -59,22 +73,347 @@ feature -- Basic operations a_handler_attached: a_handler /= Void not_if_match_star: attached req.http_if_match as l_if_match implies not l_if_match.same_string ("*") local - l_etags: LIST [READABLE_STRING_32] + l_etags: LIST [READABLE_STRING_8] + l_failed: BOOLEAN + l_date: HTTP_DATE do if attached req.http_if_match as l_if_match then - l_etags := l_if_match.split (',') - + -- TODO - also if-range when we add support for range requests + if not l_if_match.same_string ("*") then + l_etags := l_if_match.split (',') + l_failed := not across l_etags as i_etags some a_handler.matching_etag (req, i_etags.item, True) end + end + end + if l_failed then + handle_precondition_failed (req, res) else - -- TODO: check_if_unmodified_since (req, res, a_handler) + if attached req.http_if_unmodified_since as l_if_unmodified_since then + if l_if_unmodified_since.is_string_8 then + create l_date.make_from_string (l_if_unmodified_since.as_string_8) + if not l_date.has_error then + if a_handler.modified_since (req, l_date.date_time) then + handle_precondition_failed (req, res) + l_failed := True + end + end + end + end + if not l_failed then + if attached req.http_if_none_match as l_if_none_match then + l_etags := l_if_none_match.split (',') + l_failed := l_if_none_match.same_string ("*") or + across l_etags as i_etags some a_handler.matching_etag (req, i_etags.item, False) end + end + if l_failed then + handle_if_none_match_failed (req, res) + else + if attached req.http_if_modified_since as l_if_modified_since then + if l_if_modified_since.is_string_8 then + create l_date.make_from_string (l_if_modified_since.as_string_8) + if not l_date.has_error then + if not a_handler.modified_since (req, l_date.date_time) then + handle_not_modified (req, res) + l_failed := True + end + end + end + end + end + if not l_failed then + handle_content_negotiation (req, res, a_handler, False) + end + end + end + end + +feature {NONE} -- Implementation + + handle_content_negotiation (req: WSF_REQUEST; res: WSF_RESPONSE; + a_handler: WSF_SKELETON_HANDLER; a_new_resource: BOOLEAN) + -- Negotiate acceptable content for, then write, response requested by `req' into `res'. + -- Policy routines are available in `a_handler'. + -- This default version applies to GET and HEAD. + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + local + l_conneg: CONNEG_SERVER_SIDE + h: HTTP_HEADER + l_media: MEDIA_TYPE_VARIANT_RESULTS + l_lang: LANGUAGE_VARIANT_RESULTS + l_charset: CHARACTER_ENCODING_VARIANT_RESULTS + l_encoding: COMPRESSION_VARIANT_RESULTS + l_mime_types, l_langs, l_charsets, l_encodings: LIST [STRING] + l_vary_star: BOOLEAN + do + create h.make + l_vary_star := not a_handler.predictable_response (req) + if l_vary_star then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, "*") + elseif attached a_handler.additional_variant_headers (req) as l_additional then + across l_additional as i_additional loop + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, i_additional.item) + end + end + l_conneg := a_handler.conneg (req) + l_mime_types := a_handler.mime_types_supported (req) + l_media := l_conneg.media_type_preference (l_mime_types, req.http_accept) + if not l_vary_star and l_mime_types.count > 1 and attached l_media.variant_header as l_media_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_media_variant) + end + if not l_media.is_acceptable then + handle_not_accepted ("None of the requested ContentTypes were acceptable", req, res) + else + l_langs := a_handler.languages_supported (req) + l_lang := l_conneg.language_preference (l_langs, req.http_accept_language) + if not l_vary_star and l_langs.count > 1 and attached l_lang.variant_header as l_lang_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_lang_variant) + end + if not l_lang.is_acceptable then + handle_not_accepted ("None of the requested languages were acceptable", req, res) + else + if attached l_lang.language_type as l_language_type then + h.put_content_language (l_language_type) + end + l_charsets := a_handler.charsets_supported (req) + l_charset := l_conneg.charset_preference (l_charsets, req.http_accept_charset) + if not l_vary_star and l_charsets.count > 1 and attached l_charset.variant_header as l_charset_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_charset_variant) + end + if not l_charset.is_acceptable then + handle_not_accepted ("None of the requested character encodings were acceptable", req, res) + else + if attached l_media.media_type as l_media_type and attached l_charset.character_type as l_character_type then + h.put_content_type (l_media_type + "; charset=" + l_character_type) + end + l_encodings := a_handler.encodings_supported (req) + l_encoding := l_conneg.encoding_preference (l_encodings, req.http_accept_encoding) + if not l_vary_star and l_encodings.count > 1 and attached l_encoding.variant_header as l_encoding_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_encoding_variant) + end + if not l_encoding.is_acceptable then + handle_not_accepted ("None of the requested transfer encodings were acceptable", req, res) + else + if attached l_encoding.compression_type as l_compression_type then + h.put_content_encoding (l_compression_type) + end + -- We do not support multiple choices, so + send_get_response (req, res, a_handler, h, + l_media.media_type, l_lang.language_type, l_charset.character_type, l_encoding.compression_type, a_new_resource) + end + end + end + end + end + + send_get_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; + a_media_type, a_language_type, a_character_type, a_compression_type: detachable READABLE_STRING_8; a_new_resource: BOOLEAN) + -- Write response to `req' into `res' in accordance with `a_media_type' etc. + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + a_header_attached: a_header /= Void + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + local + l_chunked, l_ok: BOOLEAN + l_dt: STRING + do + a_handler.ensure_content_available (req, a_media_type, a_language_type, a_character_type, a_compression_type) + l_chunked := a_handler.is_chunking (req) + if l_chunked then + a_header.put_transfer_encoding_chunked + else + a_header.put_content_length (a_handler.content_length (req).as_integer_32) + end + if attached req.request_time as l_time then + l_dt := (create {HTTP_DATE}.make_from_date_time (l_time)).rfc1123_string + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_date, l_dt) + generate_cache_headers (req, a_handler, a_header, l_time) + end + l_ok := a_handler.response_ok (req) + if l_ok then + res.set_status_code ({HTTP_STATUS_CODE}.ok) + else + -- TODO - req.error_handler.has_error = True + --handle_internal_server_error (a_handler.last_error (req), req, res) + end + if attached a_handler.etag (req, a_media_type, a_language_type, a_character_type, a_compression_type) as l_etag then + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_etag, l_etag) + end + res.put_header_text (a_header.string) + if l_ok then + if l_chunked then + send_chunked_response (req, res, a_handler, a_header, a_media_type, a_language_type, a_character_type, a_compression_type) + else + res.put_string (a_handler.content (req, a_media_type, a_language_type, a_character_type, a_compression_type)) + end + end + end + + send_chunked_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; + a_media_type, a_language_type, a_character_type, a_compression_type: READABLE_STRING_8) + -- Write response in chunks to `req' into `res' in accordance with `a_media_type' etc. + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + local + l_chunk: TUPLE [a_chunk: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8] + do + from + if a_handler.response_ok (req) then + l_chunk := a_handler.next_chunk (req, a_media_type, a_language_type, a_character_type, a_compression_type) + res.put_chunk (l_chunk.a_chunk, l_chunk.a_extension) + else + -- TODO - req.error_handler.has_error = True + -- handle_internal_server_error (a_handler.last_error (req), req, res) + end + until + a_handler.finished (req) or not a_handler.response_ok (req) + loop + a_handler.generate_next_chunk (req, a_media_type, a_language_type, a_character_type, a_compression_type) + if a_handler.response_ok (req) then + l_chunk := a_handler.next_chunk (req, a_media_type, a_language_type, a_character_type, a_compression_type) + res.put_chunk (l_chunk.a_chunk, l_chunk.a_extension) + else + -- TODO - req.error_handler.has_error = True + -- handle_internal_server_error (a_handler.last_error (req), req, res) + end + end + if a_handler.finished (req) then + -- TODO - support for trailers + res.put_chunk_end end end - handle_precondition_failed (req: WSF_REQUEST; res: WSF_RESPONSE) - -- + generate_cache_headers (req: WSF_REQUEST; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_request_time: DATE_TIME) + -- Write headers affecting caching for `req' into `a_header'. + require + req_attached: req /= Void + a_handler_attached: a_handler /= Void + a_header_attached: a_header /= Void + a_request_time_attached: a_request_time /= Void + local + l_age, l_age_1, l_age_2: NATURAL + l_dur: DATE_TIME_DURATION + l_dt, l_field_names: STRING + do + l_age := a_handler.http_1_0_age (req) + create l_dur.make (0, 0, 0, 0, 0, l_age.as_integer_32) + l_dt := (create {HTTP_DATE}.make_from_date_time (a_request_time + l_dur)).rfc1123_string + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_expires, l_dt) + l_age_1 := a_handler.age (req) + if l_age_1 /= l_age then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "max-age=" + l_age_1.out) + end + l_age_2 := a_handler.shared_age (req) + if l_age_2 /= l_age_1 then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "s-maxage=" + l_age_2.out) + end + if a_handler.is_freely_cacheable (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "public") + elseif attached a_handler.private_headers (req) as l_fields then + l_field_names := "=" + if not l_fields.is_empty then + append_field_name (l_field_names, l_fields) + end + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "private" + l_field_names) + end + if attached a_handler.non_cacheable_headers (req) as l_fields then + l_field_names := "=" + if not l_fields.is_empty then + append_field_name (l_field_names, l_fields) + end + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "no-cache" + l_field_names) + end + if not a_handler.is_transformable (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "no-transform") + end + if a_handler.must_revalidate (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "must-revalidate") + end + if a_handler.must_proxy_revalidate (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "proxy-revalidate") + end + end + + append_field_name (a_field_names: STRING; a_fields: LIST [READABLE_STRING_8]) + -- Append all of `a_fields' as a comma-separated list to `a_field_names'. + require + a_field_names_attached: a_field_names /= Void + a_fields_attached: a_fields /= Void + do + across a_fields as i_fields loop + a_field_names.append_string (i_fields.item) + if not i_fields.is_last then + a_field_names.append_character (',') + end + end + end + +feature -- Basic operations + + handle_not_accepted (a_message: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Not Accepted response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + a_message_attached: a_message /= Void + do + -- TODO: flag this if it gets to code review. + end + + handle_if_none_match_failed (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Precondition Failed response to `res'. require req_attached: req /= Void res_attached: res /= Void do + -- TODO: flag this if it gets to code review. Why not just handle_precondition_failed? end - + + handle_not_modified (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Precondition Failed response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + do + -- TODO: flag this if it gets to code review. Why not just handle_precondition_failed? + end + + handle_precondition_failed (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a precondition failed response for `req' to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + local + h: HTTP_HEADER + do + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.precondition_failed) + res.put_header_text (h.string) + end + +note + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, 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 diff --git a/library/server/wsf/router/wsf_method_helper_factory.e b/library/server/wsf/router/wsf_method_helper_factory.e index a309b2ff..c41a6e3d 100644 --- a/library/server/wsf/router/wsf_method_helper_factory.e +++ b/library/server/wsf/router/wsf_method_helper_factory.e @@ -18,7 +18,13 @@ feature -- Factory do if a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_get) or a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_head) then - create {WSF_GET_HELPER} Result + create Result + elseif a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_put) then + create {WSF_PUT_HELPER} Result + elseif a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_post) then + create {WSF_POST_HELPER} Result + elseif a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_delete) then + create {WSF_DELETE_HELPER} Result end end diff --git a/library/server/wsf/router/wsf_post_helper.e b/library/server/wsf/router/wsf_post_helper.e new file mode 100644 index 00000000..b05a40a7 --- /dev/null +++ b/library/server/wsf/router/wsf_post_helper.e @@ -0,0 +1,70 @@ +note + + description: "[ + Policy-driven helpers to implement POST. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_POST_HELPER + +inherit + + WSF_METHOD_HELPER + rename + send_get_response as do_post + redefine + execute_new_resource, + do_post + end + +feature -- Basic operations + + execute_new_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to non-existing resource requested by `req.' into `res'. + -- Policy routines are available in `a_handler'. + local + l_etags: LIST [READABLE_STRING_32] + l_failed: BOOLEAN + do + if a_handler.allow_post_to_missing_resource (req) then + handle_content_negotiation (req, res, a_handler, True) + else + -- TODO 404 Not Found + end + end + + +feature {NONE} -- Implementation + + do_post (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; + a_media_type, a_language_type, a_character_type, a_compression_type: detachable READABLE_STRING_8; a_new_resource: BOOLEAN) + -- Write response to `req' into `res' in accordance with `a_media_type' etc. as a new URI. + do + a_handler.read_entity (req) + if a_handler.is_entity_too_large (req) then + handle_request_entity_too_large (req, res, a_handler) + else + a_handler.check_content_headers (req) + if a_handler.content_check_code (req) /= 0 then + -- TODO - 415 or 501 + else + a_handler.check_request (req, res) + if a_handler.request_check_code (req) = 0 then + a_handler.append_resource (req, res) + -- 200 or 204 or 303 or 500 (add support for this?) + -- TODO: more support, such as includes_response_entity + end + end + end + end + + handle_request_entity_too_large (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- TODO. + require + -- TODO + do + -- Need to check if condition is temporary. + end + +end diff --git a/library/server/wsf/router/wsf_previous_policy.e b/library/server/wsf/router/wsf_previous_policy.e index 9de00906..b64c0450 100644 --- a/library/server/wsf/router/wsf_previous_policy.e +++ b/library/server/wsf/router/wsf_previous_policy.e @@ -7,11 +7,11 @@ note date: "$Date$" revision: "$Revision$" -class WSF_PREVIOUS_POLICY +deferred class WSF_PREVIOUS_POLICY feature -- Access - resource_previously_existed (req: WSF_REQUEST) : BOOLEAN + resource_previously_existed (req: WSF_REQUEST): BOOLEAN -- Did `req.path_translated' exist previously? require req_attached: req /= Void @@ -19,7 +19,7 @@ feature -- Access -- No. Override if this is not want you want. end - resource_moved_permanently (req: WSF_REQUEST) : BOOLEAN + resource_moved_permanently (req: WSF_REQUEST): BOOLEAN -- Was `req.path_translated' moved permanently? require req_attached: req /= Void @@ -28,7 +28,7 @@ feature -- Access -- No. Override if this is not want you want. end - resource_moved_temporarily (req: WSF_REQUEST) : BOOLEAN + resource_moved_temporarily (req: WSF_REQUEST): BOOLEAN -- Was `req.path_translated' moved temporarily? require req_attached: req /= Void @@ -37,4 +37,26 @@ feature -- Access -- No. Override if this is not want you want. end + previous_location (req: WSF_REQUEST): LIST [URI] + -- Previous location(s) for resource named by `req'; + require + req_attached: req /= Void + previously_existed: resource_previously_existed (req) + moved: resource_moved_permanently (req) or resource_moved_temporarily (req) + deferred + ensure + previous_location_attached: Result /= Void + non_empty_list: not Result.empty + end + +note + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, 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 diff --git a/library/server/wsf/router/wsf_put_helper.e b/library/server/wsf/router/wsf_put_helper.e new file mode 100644 index 00000000..67f123b8 --- /dev/null +++ b/library/server/wsf/router/wsf_put_helper.e @@ -0,0 +1,75 @@ +note + + description: "[ + Policy-driven helpers to implement PUT. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_PUT_HELPER + +inherit + + WSF_METHOD_HELPER + rename + send_get_response as do_put + redefine + execute_new_resource, + do_put + end + +feature -- Basic operations + + execute_new_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to non-existing resource requested by `req.' into `res'. + -- Policy routines are available in `a_handler'. + do + if a_handler.treat_as_moved_permanently (req) then + -- TODO 301 Moved permanently response (single location) + else + handle_content_negotiation (req, res, a_handler, True) + end + end + + +feature {NONE} -- Implementation + + do_put (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; + a_media_type, a_language_type, a_character_type, a_compression_type: detachable READABLE_STRING_8; a_new_resource: BOOLEAN) + -- Write response to `req' into `res' in accordance with `a_media_type' etc. as a new URI. + do + a_handler.read_entity (req) + if a_handler.is_entity_too_large (req) then + handle_request_entity_too_large (req, res, a_handler) + else + a_handler.check_content_headers (req) + if a_handler.content_check_code (req) /= 0 then + -- TODO - 415 or 501 + else + a_handler.check_request (req, res) + if a_handler.request_check_code (req) = 0 then + if a_new_resource then + a_handler.create_resource (req, res) + -- 201 or 500 (add support for this?) + else + a_handler.check_conflict (req, res) + if a_handler.conflict_check_code (req) = 0 then + a_handler.update_resource (req, res) + -- 204 or 500 (add support for this?) + -- TODO: more support, such as includes_response_entity + end + end + end + end + end + end + + handle_request_entity_too_large (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- TODO. + require + -- TODO + do + -- Need to check if condition is temporary. + end + +end diff --git a/library/server/wsf/router/wsf_skeleton_handler.e b/library/server/wsf/router/wsf_skeleton_handler.e index a6389ee8..aa8d01c3 100644 --- a/library/server/wsf/router/wsf_skeleton_handler.e +++ b/library/server/wsf/router/wsf_skeleton_handler.e @@ -20,8 +20,358 @@ inherit WSF_PREVIOUS_POLICY + WSF_CACHING_POLICY + WSF_METHOD_HELPER_FACTORY +feature -- Access + + is_chunking (req: WSF_REQUEST): BOOLEAN + -- Will the response to `req' using chunked transfer encoding? + require + req_attached: req /= Void + deferred + end + + includes_response_entity (req: WSF_REQUEST): BOOLEAN + -- Does the response to `req' include an entity? + -- Method will be DELETE, OUT, POST or an extension method. + require + req_attached: req /= Void + deferred + end + + conneg (req: WSF_REQUEST): CONNEG_SERVER_SIDE + -- Content negotiatior for `req'; + -- This would normally be a once object, ignoring `req'. + require + req_attached: req /= Void + deferred + end + + mime_types_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + mime_types_supported_includes_default: Result.has (conneg (req).mime_default) + end + + languages_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Language header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + languages_supported_includes_default: Result.has (conneg (req).language_default) + end + + charsets_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Charset header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + charsets_supported_includes_default: Result.has (conneg (req).charset_default) + end + + encodings_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Encoding header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + encodings_supported_includes_default: Result.has (conneg (req).encoding_default) + end + + additional_variant_headers (req: WSF_REQUEST): detachable LIST [STRING] + -- Header other than Accept, Accept-Language, Accept-Charset and Accept-Encoding, + -- which might affect the response + do + end + + predictable_response (req: WSF_REQUEST): BOOLEAN + -- Does the response to `req' vary only on the dimensions of ContentType, Language, Charset and Transfer encoding, + -- plus those named in `additional_variant_headers'? + do + Result := True + -- redefine to return `False', so as to induce a Vary: * header + end + + matching_etag (req: WSF_REQUEST; a_etag: READABLE_STRING_32; a_strong: BOOLEAN): BOOLEAN + -- Is `a_etag' a match for resource requested in `req'? + -- If `a_strong' then the strong comparison function must be used. + require + req_attached: req /= Void + deferred + end + + etag (req: WSF_REQUEST; a_media_type, a_language_type, a_character_type, a_compression_type: READABLE_STRING_8): detachable READABLE_STRING_8 + -- Optional Etag for `req' in the requested variant + require + req_attached: req /= Void + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + deferred + end + + modified_since (req: WSF_REQUEST; a_date_time: DATE_TIME): BOOLEAN + -- Has resource requested in `req' been modified since `a_date_time' (UTC)? + require + req_attached: req /= Void + deferred + end + + treat_as_moved_permanently (req: WSF_REQUEST): BOOLEAN + -- Rather than store as a new entity, do we treat it as an existing entity that has been moved? + require + req_attached: req /= Void + put_request: req.is_request_method ({HTTP_REQUEST_METHODS}.method_put) + do + -- No. Redefine this if needed. + end + + allow_post_to_missing_resource (req: WSF_REQUEST): BOOLEAN + -- The resource named in `req' does not exist, and this is a POST. Do we allow it? + require + req_attached: req /= Void + deferred + end + +feature -- Measurement + + content_length (req: WSF_REQUEST): NATURAL + -- Length of entity-body of the response to `req' + require + req_attached: req /= Void + not_chunked: not is_chunking (req) + deferred + end + +feature -- Status report + + finished (req: WSF_REQUEST): BOOLEAN + -- Has the last chunk been generated for `req'? + require + req_attached: req /= Void + chunked: is_chunking (req) + deferred + end + +feature -- DELETE + + delete (req: WSF_REQUEST) + -- Delete resource named in `req' or set an error on `req.error_handler'. + require + req_attached: req /= Void + deferred + ensure + error_or_queued_or_deleted: not req.error_handler.has_error implies (delete_queued (req) or deleted (req)) + end + + deleted (req: WSF_REQUEST): BOOLEAN + -- Has resource named by `req' been deleted? + require + req_attached: req /= Void + deferred + end + + delete_queued (req: WSF_REQUEST): BOOLEAN + -- Has resource named by `req' been queued for deletion? + require + req_attached: req /= Void + deferred + ensure + entity_available: includes_response_entity (req) + end + +feature -- GET/HEAD content + + ensure_content_available (req: WSF_REQUEST; + a_media_type, a_language_type, a_character_type, a_compression_type: READABLE_STRING_8) + -- Commence generation of response text (entity-body). + -- If not chunked, then this will create the entire entity-body so as to be available + -- for a subsequent call to `content'. + -- If chunked, only the first chunk will be made available to `next_chunk'. If chunk extensions + -- are used, then this will also generate the chunk extension for the first chunk. + require + req_attached: req /= Void + get_or_head: req.is_get_head_request_method + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + deferred + end + + response_ok (req: WSF_REQUEST): BOOLEAN + -- Has generation of the response (so-far, if chunked) proceeded witout error? + require + req_attached: req /= Void + do + Result := not req.error_handler.has_error + ensure + last_error_set: Result = not req.error_handler.has_error + end + + content (req: WSF_REQUEST; a_media_type, a_language_type, a_character_type, a_compression_type: READABLE_STRING_8): READABLE_STRING_8 + -- Non-chunked entity body in response to `req' + require + req_attached: req /= Void + head_get_or_delete: req.is_get_head_request_method or req.is_delete_request_method + no_error: response_ok (req) + not_chunked: not is_chunking (req) + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + deferred + end + + generate_next_chunk (req: WSF_REQUEST; a_media_type, a_language_type, a_character_type, a_compression_type: READABLE_STRING_8) + -- Prepare next chunk (including optional chunk extension) of entity body in response to `req'. + -- This is not called for the first chunk. + require + req_attached: req /= Void + no_error: response_ok (req) + chunked: is_chunking (req) + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + deferred + end + + next_chunk (req: WSF_REQUEST; a_media_type, a_language_type, a_character_type, a_compression_type: READABLE_STRING_8): TUPLE [a_check: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8] + -- Next chunk of entity body in response to `req'; + -- The second field of the result is an optional chunk extension. + require + req_attached: req /= Void + no_error: response_ok (req) + chunked: is_chunking (req) + a_media_type_attached: a_media_type /= Void + a_language_type_attached: a_language_type /= Void + a_character_type_attached: a_character_type /= Void + a_compression_type_attached: a_compression_type /= Void + deferred + end + +feature -- PUT/POST + + read_entity (req: WSF_REQUEST) + -- Read request body and set as `req.execution_variable ("REQUEST_ENTITY")'. + require + req_attached: req /= Void + local + l_body: STRING + do + create l_body.make_empty + req.read_input_data_into (l_body) + if not l_body.is_empty then + req.set_execution_variable ("REQUEST_ENTITY", l_body) + end + end + + is_entity_too_large (req: WSF_REQUEST): BOOLEAN + -- Is the entity stored in `req.execution_variable ("REQUEST_ENTITY")' too large for the application? + require + req_attached: req /= Void + deferred + end + + check_content_headers (req: WSF_REQUEST) + -- Check we can support all content headers on request entity. + -- Set `req.execution_variable ("CONTENT_CHECK_CODE")' to {NATURAL} zero if OK, or 415 or 501 if not. + require + req_attached: req /= Void + deferred + end + + content_check_code (req: WSF_REQUEST): NATURAL + -- Code set by `check_content_headers'. + require + req_attached: req /= Void + do + if attached {NATURAL} req.execution_variable ("CONTENT_CHECK_CODE") as l_code then + Result := l_code + end + end + + create_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Create new resource in response to a PUT request when `check_resource_exists' returns `False'. + -- Implementor must set error code of 200 OK or 500 Server Error. + require + req_attached: req /= Void + res_attached: res /= Void + put_request: req.is_put_request_method + deferred + end + + append_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Create new resource in response to a POST request. + -- Implementor must set error code of 200 OK or 204 No Content or 303 See Other or 500 Server Error. + require + req_attached: req /= Void + res_attached: res /= Void + post_request: req.is_post_request_method + deferred + end + + check_conflict (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Check we can support all content headers on request entity. + -- Set `req.execution_variable ("CONFLICT_CHECK_CODE")' to {NATURAL} zero if OK, or 409 if not. + -- In the latter case, write the full error response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + deferred + end + + conflict_check_code (req: WSF_REQUEST): NATURAL + -- Code set by `check_conflict'. + require + req_attached: req /= Void + do + if attached {NATURAL} req.execution_variable ("CONFLICT_CHECK_CODE") as l_code then + Result := l_code + end + end + + check_request (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Check that the request entity is a valid request. + -- The entity is available as `req.execution_variable ("REQUEST_ENTITY")'. + -- Set `req.execution_variable ("REQUEST_CHECK_CODE")' to {NATURAL} zero if OK, or 400 if not. + -- In the latter case, write the full error response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + put_or_post: req.is_put_post_request_method + deferred + end + + request_check_code (req: WSF_REQUEST): NATURAL + -- Code set by `check_request'. + require + req_attached: req /= Void + do + if attached {NATURAL} req.execution_variable ("REQUEST_CHECK_CODE") as l_code then + Result := l_code + end + end + + update_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Perform the update requested in `req'. + -- Write a response to `res' with a code of 204 or 500. + require + req_attached: req /= Void + res_attached: res /= Void + put_request: req.is_put_request_method + deferred + end + feature -- Execution execute (req: WSF_REQUEST; res: WSF_RESPONSE) @@ -66,14 +416,14 @@ feature -- Execution check_resource_exists (req: WSF_REQUEST; a_helper: WSF_METHOD_HELPER) -- Call `a_helper.set_resource_exists' to indicate that `req.path_translated' -- is the name of an existing resource. - -- Optionally, also call `req.set_server_data', if this is now available as a by-product - -- of the existence check. require req_attached: req /= Void a_helper_attached: a_helper /= Void deferred end +feature {NONE} -- Implementation + handle_internal_server_error (res: WSF_RESPONSE) -- Write "Internal Server Error" response to `res'. require @@ -94,5 +444,16 @@ feature -- Execution status_is_service_unavailable: res.status_code = {HTTP_STATUS_CODE}.internal_server_error body_sent: res.message_committed and then res.transfered_content_length > 0 end - + + +note + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, 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