doze 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/README +6 -0
  2. data/lib/doze/application.rb +92 -0
  3. data/lib/doze/collection/object.rb +14 -0
  4. data/lib/doze/entity.rb +62 -0
  5. data/lib/doze/error.rb +75 -0
  6. data/lib/doze/media_type.rb +135 -0
  7. data/lib/doze/negotiator.rb +107 -0
  8. data/lib/doze/request.rb +119 -0
  9. data/lib/doze/resource/error.rb +21 -0
  10. data/lib/doze/resource/proxy.rb +81 -0
  11. data/lib/doze/resource.rb +193 -0
  12. data/lib/doze/responder/error.rb +34 -0
  13. data/lib/doze/responder/main.rb +41 -0
  14. data/lib/doze/responder/resource.rb +262 -0
  15. data/lib/doze/responder.rb +58 -0
  16. data/lib/doze/response.rb +78 -0
  17. data/lib/doze/router/anchored_route_set.rb +68 -0
  18. data/lib/doze/router/route.rb +88 -0
  19. data/lib/doze/router/route_set.rb +34 -0
  20. data/lib/doze/router.rb +100 -0
  21. data/lib/doze/serialization/entity.rb +34 -0
  22. data/lib/doze/serialization/form_data_helpers.rb +40 -0
  23. data/lib/doze/serialization/html.rb +116 -0
  24. data/lib/doze/serialization/json.rb +29 -0
  25. data/lib/doze/serialization/multipart_form_data.rb +162 -0
  26. data/lib/doze/serialization/resource.rb +30 -0
  27. data/lib/doze/serialization/resource_proxy.rb +14 -0
  28. data/lib/doze/serialization/www_form_encoded.rb +42 -0
  29. data/lib/doze/serialization/yaml.rb +25 -0
  30. data/lib/doze/uri_template.rb +220 -0
  31. data/lib/doze/utils.rb +53 -0
  32. data/lib/doze/version.rb +3 -0
  33. data/lib/doze.rb +5 -0
  34. data/test/functional/auth_test.rb +69 -0
  35. data/test/functional/base.rb +159 -0
  36. data/test/functional/cache_header_test.rb +76 -0
  37. data/test/functional/direct_response_test.rb +16 -0
  38. data/test/functional/error_handling_test.rb +131 -0
  39. data/test/functional/get_and_conneg_test.rb +182 -0
  40. data/test/functional/media_type_extensions_test.rb +102 -0
  41. data/test/functional/media_type_test.rb +40 -0
  42. data/test/functional/method_support_test.rb +49 -0
  43. data/test/functional/non_get_method_test.rb +173 -0
  44. data/test/functional/precondition_test.rb +84 -0
  45. data/test/functional/raw_path_info_test.rb +69 -0
  46. data/test/functional/resource_representation_test.rb +14 -0
  47. data/test/functional/router_test.rb +196 -0
  48. data/test/functional/serialization_test.rb +142 -0
  49. data/test/functional/uri_template_test.rb +51 -0
  50. metadata +221 -0
data/README ADDED
@@ -0,0 +1,6 @@
1
+ # Doze
2
+
3
+ RESTful resource-oriented API framework
4
+
5
+ # Example application
6
+
@@ -0,0 +1,92 @@
1
+ require 'time' # httpdate
2
+ require 'doze/utils'
3
+ require 'doze/error'
4
+ require 'doze/uri_template'
5
+ require 'doze/request'
6
+ require 'doze/router'
7
+ require 'doze/resource'
8
+ require 'doze/entity'
9
+ require 'doze/resource/error'
10
+ require 'doze/resource/proxy'
11
+ require 'doze/request'
12
+ require 'doze/response'
13
+ require 'doze/responder'
14
+ require 'doze/responder/main'
15
+ require 'doze/responder/error'
16
+ require 'doze/responder/resource'
17
+ require 'doze/negotiator'
18
+
19
+ class Doze::Application
20
+ include Doze::Utils
21
+
22
+ DEFAULT_CONFIG = {
23
+ :error_resource_class => Doze::Resource::Error,
24
+
25
+ # Setting this to false is useful for testing, so an exception can make a test fail via
26
+ # the normal channels rather than having to check and parse it out of a response.
27
+ :catch_application_errors => true,
28
+
29
+ # useful for development
30
+ :expose_exception_details => true,
31
+
32
+ # Methods not included here will be rejected with 'method not implemented'
33
+ # before any resource is called. (methods included here may still be rejected
34
+ # as not supported by individual resources via supports_method).
35
+ # Note: HEAD is supported as part of GET support, and OPTIONS comes for free.
36
+ :recognized_methods => [:get, :post, :put, :delete],
37
+
38
+ # You might need to change this depending on what rack middleware you use to
39
+ # authenticate / identify users. Eg could use
40
+ # env['rack.session'] for use with Rack::Session (the default)
41
+ # env['REMOTE_USER'] for use with Rack::Auth::Basic / Digest, and direct via Apache and some other front-ends that do http auth
42
+ # env['rack.auth.openid'] for use with Rack::Auth::OpenID
43
+ # This is used to look up a session or user object in the rack environment
44
+ :session_from_rack_env => proc {|env| env['rack.session']},
45
+
46
+ # Used to determine whether the user/session object obtained via :session_from_rack_env is to be treated as authenticated or not.
47
+ # By default this looks for a key :user within a session object.
48
+ # For eg env['REMOTE_USER'] the session is the authenticated username and you probably just want to test for !session.nil?
49
+ :session_authenticated => proc {|session| !session[:user].nil?},
50
+
51
+ # Doze has a special facility to use a file extension style suffix on the URI.
52
+ # Instead of passing this file extension through the routing process, it is dropped from the routed URI
53
+ # and handled specially, effectively overriding the Accept header for that request and forcing a response
54
+ # with the media type in question.
55
+ # Relies on media types being registered by extension, see Doze::MediaType.
56
+ # NB: if you enable this setting, be aware that extensions are stripped prior to routing, so you will
57
+ # lose the ability to route based on file extensions, and must be careful to escape the extension delimiter (".")
58
+ # when putting a text fragment for matching at the end of a uri.
59
+ :media_type_extensions => false
60
+ }
61
+
62
+ attr_reader :config, :root, :logger
63
+
64
+ # root may be a Router, a Resource, or both.
65
+ # If a resource, its uri should return '/'
66
+ def initialize(root, config={})
67
+ @config = DEFAULT_CONFIG.merge(config)
68
+ @logger = @config[:logger] || STDOUT
69
+ @root = root
70
+
71
+ # This is done on application initialization to ensure that statically-known
72
+ # information about routing paths is propagated as far as possible, so that
73
+ # resource instances can know their uris without necessarily having been
74
+ # a part of the routing chain for the current request.
75
+ # TODO use SCRIPT_NAME from the rack env of the first request we get, rather than ''
76
+ @root.propagate_static_routes('') if @root.respond_to?(:propagate_static_routes)
77
+ end
78
+
79
+ def call(env)
80
+ begin
81
+ request = Doze::Request.new(self, env)
82
+ responder = Doze::Responder::Main.new(self, request)
83
+ responder.call
84
+ rescue => exception
85
+ raise unless config[:catch_application_errors]
86
+ lines = ['500 response via error resource failed', "#{exception.class}: #{exception.message}", *exception.backtrace]
87
+ @logger << lines.join("\n")
88
+ body = config[:expose_exception_details] ? lines : [lines.first]
89
+ [STATUS_INTERNAL_SERVER_ERROR, {}, [body.join("\n")]]
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,14 @@
1
+ module Doze::Collection::Object
2
+
3
+ include Doze::Serialization::Resource
4
+
5
+ def get_data
6
+ end
7
+
8
+ def get_property(property)
9
+ end
10
+
11
+ def put_property(property, data)
12
+ end
13
+
14
+ end
@@ -0,0 +1,62 @@
1
+ require 'digest/md5'
2
+
3
+ # A simple wrapper class for an entity, which is essentially a lump of binary data
4
+ # together with some metadata about it, most importantly a MediaType, but also
5
+ # potentially a character encoding and a content language.
6
+ #
7
+ # A key feature is that the binary data may be specified lazily via a block.
8
+ # This is so that content negotiation can demand the data only once it's decided
9
+ # which (if any) of many proferred entities it wants to respond with.
10
+ #
11
+ # TODO: handle character encodings here in a nicer 1.9-compatible way
12
+ # TODO: maybe allow a stream for lazy_binary_data too
13
+ class Doze::Entity
14
+ DEFAULT_TEXT_ENCODING = 'iso-8859-1'
15
+
16
+ attr_reader :media_type, :media_type_params, :encoding, :language, :binary_data_length
17
+
18
+ # Used when constructing a HTTP response from this entity
19
+ # For when you need to specify extra entity-content-specific HTTP headers to be included when
20
+ # responding with this entity.
21
+ # A teensy bit of an abstraction leak, but helpful for awkward cases.
22
+ attr_reader :extra_content_headers
23
+
24
+ class << self; alias :new_from_binary_data :new; end
25
+
26
+ def initialize(media_type, options={}, &lazy_binary_data)
27
+ @binary_data = options[:binary_data]
28
+ @binary_data_stream = options[:binary_data_stream]
29
+ @binary_data_length = options[:binary_data_length]
30
+ @lazy_binary_data = options[:lazy_binary_data] || lazy_binary_data
31
+
32
+ @media_type = media_type
33
+ @media_type_params = options[:media_type_params] || {}
34
+ @encoding = options[:encoding] || (DEFAULT_TEXT_ENCODING if @media_type.major == 'text')
35
+ @language = options[:language]
36
+ @extra_content_headers = options[:extra_content_headers] || {}
37
+ end
38
+
39
+ def binary_data_stream
40
+ @binary_data_stream ||= if @binary_data
41
+ StringIO.new(@binary_data)
42
+ end
43
+ end
44
+
45
+ def binary_data
46
+ @binary_data ||= if @lazy_binary_data
47
+ @lazy_binary_data.call
48
+ elsif @binary_data_stream
49
+ @binary_data_stream.rewind if @binary_data_stream.respond_to?(:rewind)
50
+ @binary_data_stream.read
51
+ end
52
+ end
53
+
54
+ # This is a 'strong' etag in that it's sensitive to the exact bytes of the entity.
55
+ # Note that etags are per-entity, not per-resource. (even weak etags, which we don't yet support;
56
+ # 'weak' appears to refer to time-based equivalence for the same entity, rather than equivalence of all entity representations of a resource.)
57
+ #
58
+ # May return nil. Default implementation is an MD5 digest of the entity data.
59
+ def etag
60
+ Digest::MD5.hexdigest(binary_data)
61
+ end
62
+ end
data/lib/doze/error.rb ADDED
@@ -0,0 +1,75 @@
1
+ # Doze::Error wraps the data required to send an HTTP error response as an exception which Application and Responder infrastructure can raise.
2
+ class Doze::Error < StandardError
3
+ def initialize(http_status=Doze::Utils::STATUS_INTERNAL_SERVER_ERROR, message='', headers={}, backtrace=nil)
4
+ @http_status = http_status
5
+ @headers = headers
6
+ @backtrace = backtrace
7
+ super(Rack::Utils::HTTP_STATUS_CODES[http_status] + (message ? ": #{message}" : ''))
8
+ end
9
+
10
+ def backtrace
11
+ @backtrace || super
12
+ end
13
+
14
+ attr_reader :http_status, :headers
15
+ end
16
+
17
+ # Errors intended to be raised within resource or entity code to indicate a client error.
18
+ # Currently this is a subclass of the internally-used Doze::Error class, but could
19
+ # equally be a separate exception class intended for Resource-level use which is caught and
20
+ # re-raised by the internal code.
21
+ class Doze::ClientError < Doze::Error; end
22
+
23
+ # An error parsing a submitted Entity representation. Should typically only be raised within Entity code
24
+ class Doze::ClientEntityError < Doze::ClientError
25
+ def initialize(message=nil)
26
+ super(Doze::Utils::STATUS_BAD_REQUEST, message)
27
+ end
28
+ end
29
+
30
+ # Unbeknownst at the time of routing, the resource is not actually there.
31
+ class Doze::ResourceNotFoundError < Doze::ClientError
32
+ def initialize(message=nil)
33
+ super(Doze::Utils::STATUS_NOT_FOUND, message)
34
+ end
35
+ end
36
+
37
+ # Can be used for any error at the resource level which is caused by client error.
38
+ # Should relate to a problem processing the resource-level semantics of a request,
39
+ # rather than a syntactic error in a submitted entity representation.
40
+ # see http://tools.ietf.org/html/rfc4918#section-11.2
41
+ class Doze::ClientResourceError < Doze::ClientError
42
+ def initialize(message=nil)
43
+ super(Doze::Utils::STATUS_UNPROCESSABLE_ENTITY, message)
44
+ end
45
+ end
46
+
47
+ # Can be used if you want to deny an action, but you couldn't do it at the time
48
+ # of routing (which you could have done with Router#authorize_routing) or if you
49
+ # want to include an error reason with the response
50
+ class Doze::UnauthorizedError < Doze::ClientError
51
+ def initialize(reason='unauthorized')
52
+ super(Doze::Utils::STATUS_UNAUTHORIZED, reason)
53
+ end
54
+ end
55
+ class Doze::ForbiddenError < Doze::ClientError
56
+ def initialize(reason='forbidden')
57
+ super(Doze::Utils::STATUS_FORBIDDEN, reason)
58
+ end
59
+ end
60
+
61
+ # You can raise this if there is some problem internally that can't be handled
62
+ # by the resource
63
+ class Doze::ServerError < Doze::Error; end
64
+
65
+ # The resource might exist, but for some reason the requested operation
66
+ # can not be performed at this moment in time. This type of error would
67
+ # indicate that this is a temporary situation and is due, like the error says,
68
+ # to the resource, or some dependency of it, being unavailable.
69
+ # This translates to a 503, innit.
70
+ class Doze::ResourceUnavailableError < Doze::ServerError
71
+ def initialize(message=nil)
72
+ super(Doze::Utils::STATUS_SERVICE_UNAVAILABLE, message)
73
+ end
74
+ end
75
+
@@ -0,0 +1,135 @@
1
+ class Doze::MediaType
2
+ NAME_LOOKUP = {}
3
+ BY_EXTENSION = {}
4
+
5
+ # Names for this media type.
6
+ # Names should uniquely identify the media type, so eg [audio/x-mpeg3, audio/mp3] might both be names of one
7
+ # media type, but application/xml is not a name of application/xhtml+xml; see matches_names
8
+ attr_reader :names
9
+
10
+ # The primary name for this media type.
11
+ def name; @names.first; end
12
+
13
+ # Media type strings which this media type matches.
14
+ # Matching means this media type is acceptable in reponse to a request for the media type string in question.
15
+ # Eg application/xhtml+xml is acceptable in response to a request for application/xml or text/xml,
16
+ # so text/xml and application/xml may be listed under the matches_names of application/xhtml+xml
17
+ attr_reader :matches_names
18
+
19
+ # The name used to describe this media type to clients (sometimes we want to use a more
20
+ # detailed media type internally). Defaults to name
21
+ def output_name
22
+ @output_name || @names.first
23
+ end
24
+
25
+ # Media types may be configured to use a different entity class to the default (Doze::Entity) for an
26
+ # entity of that media type
27
+ attr_reader :entity_class
28
+
29
+ # Some serialization media types have a plus suffix which can be used to create derived types, eg
30
+ # application/xml, with plus_suffix 'xml', could have application/xhtml+xml as a derived type
31
+ # see register_derived_type
32
+ attr_reader :plus_suffix
33
+
34
+ # Media type may be associated with a particular file extension, eg image/jpeg with .jpeg
35
+ # Registered media types may be looked up by extension, eg this is used when :media_type_extensions
36
+ # is enabled on the application.
37
+ #
38
+ # If you register more than one media type with the same extension the most recent one will
39
+ # take priority, ie probably best not to do this.
40
+ attr_reader :extension
41
+
42
+ # Creates and registers a media_type instance by its names for lookup via [].
43
+ # This means this instance will be used when a client submits an entity with any of the given
44
+ # names.
45
+ # You're recommended to register any media types that are frequently used as well,
46
+ # even if you don't need any special options or methods for them.
47
+ def self.register(name, options={})
48
+ new(name, options).register!
49
+ end
50
+
51
+ def register!
52
+ names.each do |n|
53
+ raise "Attempt to register media_type name #{n} twice" if NAME_LOOKUP.has_key?(n)
54
+ NAME_LOOKUP[n] = self
55
+ end
56
+ register_extension!
57
+ self
58
+ end
59
+
60
+ def register_extension!
61
+ BY_EXTENSION[@extension] = self if @extension
62
+ end
63
+
64
+ def self.[](name)
65
+ NAME_LOOKUP[name] || new(name)
66
+ end
67
+
68
+ # name: primary name for the media type
69
+ # options:
70
+ # :aliases :: extra names to add to #names
71
+ # :output_name :: defaults to name
72
+ # :also_matches :: extra names to add to matches_names, in addition to names and output_name
73
+ # :entity_class
74
+ # :plus_suffix
75
+ # :extension
76
+ def initialize(name, options={})
77
+ @names = [name]
78
+ @names.push(*options[:aliases]) if options[:aliases]
79
+
80
+ @output_name = options[:output_name]
81
+
82
+ @matches_names = @names.dup
83
+ @matches_names << @output_name if @output_name
84
+ @matches_names.push(*options[:also_matches]) if options[:also_matches]
85
+ @matches_names.uniq!
86
+
87
+ @entity_class = options[:entity_class] || Doze::Entity
88
+ @plus_suffix = options[:plus_suffix]
89
+
90
+ @extension = options[:extension]
91
+ end
92
+
93
+ def major
94
+ @major ||= name.split('/', 2)[0]
95
+ end
96
+
97
+ def minor
98
+ @major ||= name.split('/', 2)[1]
99
+ end
100
+
101
+ # Helper to derive eg application/vnd.foo+json from application/json and name_prefix application/vnd.foo
102
+ def register_derived_type(name_prefix, options={})
103
+ options = {
104
+ :also_matches => [],
105
+ :entity_class => @entity_class
106
+ }.merge!(options)
107
+ options[:also_matches].push(*self.matches_names)
108
+ name = @plus_suffix ? "#{name_prefix}+#{plus_suffix}" : name_prefix
109
+ self.class.register(name, options)
110
+ end
111
+
112
+ # Create a new entity of this media_type. Uses entity_class
113
+ def new_entity(options, &b)
114
+ @entity_class.new(self, options, &b)
115
+ end
116
+
117
+ def subtype?(other)
118
+ @matches_names.include?(other.name)
119
+ end
120
+
121
+ def matches_prefix?(prefix)
122
+ @matches_names.any? {|name| name.start_with?(prefix)}
123
+ end
124
+
125
+ # Equality override to help in case multiple temporary instances of a media type of a given name are compared.
126
+ def ==(other)
127
+ super || (other.is_a?(Doze::MediaType) && other.name == name)
128
+ end
129
+
130
+ alias :eql? :==
131
+
132
+ def hash
133
+ name.hash
134
+ end
135
+ end
@@ -0,0 +1,107 @@
1
+ # A Negotiator handles content negotiation on behalf of a client request.
2
+ # It will choose the entity it prefers from a list of options offered to it.
3
+ # You can ask it to give you a quality value, or to choose from a list of options.
4
+ # It'll choose from media_types, languages, or combinations of the two.
5
+ class Doze::Negotiator
6
+ def initialize(request, ignore_unacceptable_accepts=false)
7
+ @ignore_unacceptable_accepts = ignore_unacceptable_accepts
8
+
9
+ @media_type_criterea = if (ext = request.extension)
10
+ # if the request extension requests a specific media type, this overrides any Accept header and is
11
+ # interpreted as a demand for this media type and this one only.
12
+ media_type = Doze::MediaType::BY_EXTENSION[ext]
13
+ if media_type
14
+ [[media_type.name, 2, 1.0]]
15
+ else
16
+ # if there's a request extension but we can't find a media type for it, we interpret this as an
17
+ # 'impossible demand' that will match nothing
18
+ []
19
+ end
20
+
21
+ elsif (accept_header = request.env['HTTP_ACCEPT'])
22
+ parse_accept_header(accept_header) {|range| matcher_from_media_range(range)}.sort_by {|matcher,specificity,q| -specificity}
23
+
24
+ else
25
+ # No Accept header - anything will do
26
+ [[Object, 0, 1.0]]
27
+ end
28
+
29
+ accept_language_header = request.env['HTTP_ACCEPT_LANGUAGE']
30
+ @language_criterea = if accept_language_header
31
+ parse_accept_header(accept_language_header) {|range| matcher_from_language_range(range)}.sort_by {|matcher,specificity,q| -specificity} + [[nil, 0, 0.001]]
32
+ # When language_criterea are given, we allow a low-specificity tiny-but-nonzero match for a language of 'nil', ie entities with no
33
+ # language, even though the accept header may appear to require a particular language. Because it makes no sense to apply Accept-Language
34
+ # criterea to resources whose representations aren't language-specific.
35
+ else
36
+ [[Object, 0, 1.0]]
37
+ end
38
+ end
39
+
40
+ def media_type_quality(media_type)
41
+ @media_type_criterea.each {|matcher,specificity,quality| return quality if media_type.matches_names.any? {|name| matcher === name}}; 0
42
+ end
43
+
44
+ def language_quality(language)
45
+ @language_criterea.each {|matcher,specificity,quality| return quality if matcher === language}; 0
46
+ end
47
+
48
+ # Combined quality value for a (media_type, language) pair
49
+ def quality(media_type, language)
50
+ media_type_quality(media_type)*language_quality(language)
51
+ end
52
+
53
+ # Choose from a list of Doze::Entity
54
+ def choose_entity(entities)
55
+ max_by_non_zero(entities) {|a| quality(a.media_type, a.language)}
56
+ end
57
+
58
+ private
59
+ # Given an http-style media-range, language-range, charset-range etc string, return a ruby object which answers to ===(string)
60
+ # for whether or not that string matches the range given. (note: these are useful in combination with Enumerable#grep)
61
+ # together with a priority value for the priority of this matcher (most specific has highest priority)
62
+ # Example input: *, text/*, text/html, en, en-gb, utf-8
63
+ def matcher_from_media_range(range_string)
64
+ case range_string
65
+ when '*', '*/*'
66
+ # Object === 'anything'
67
+ [Object, 0]
68
+ when /^(.*?\/)\*$/
69
+ # media type range eg text/*
70
+ [/^#{Regexp.escape($1)}/, 1]
71
+ else
72
+ [range_string, 2]
73
+ end
74
+ end
75
+
76
+ def matcher_from_language_range(range_string)
77
+ case range_string
78
+ when '*'
79
+ # Object === 'anything'
80
+ [Object, 0]
81
+ else
82
+ # en matches en, en-gb, en-whatever-else-after-a-hyphen, with longer strings more specific
83
+ [/^#{Regexp.escape(range_string)}(-|$)/, range_string.length]
84
+ end
85
+ end
86
+
87
+ def parse_accept_header(accept_header_value)
88
+ accept_header_value.split(/,\s*/).map do |part|
89
+ /^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) or next # From WEBrick via Rack
90
+ q = ($2 || 1.0).to_f
91
+ matcher, specificity = yield($1)
92
+ [matcher, specificity, q]
93
+ end.compact
94
+ end
95
+
96
+ def max_by_non_zero(array)
97
+ max_quality = 0; max_item = nil
98
+ array.each do |item|
99
+ quality = yield(item)
100
+ if quality > max_quality
101
+ max_quality = quality
102
+ max_item = item
103
+ end
104
+ end
105
+ max_item || (@ignore_unacceptable_accepts && array.first)
106
+ end
107
+ end
@@ -0,0 +1,119 @@
1
+ require 'doze/error'
2
+ require 'doze/utils'
3
+
4
+ # Some helpers for Rack::Request
5
+ class Doze::Request < Rack::Request
6
+ def initialize(app, env)
7
+ @app = app
8
+ super(env)
9
+ end
10
+
11
+ attr_reader :app
12
+
13
+ # this delibarately ignores the HEAD vs GET distinction; use head? to check
14
+ def normalized_request_method
15
+ method = @env["REQUEST_METHOD"]
16
+ method == 'HEAD' ? 'get' : method.downcase
17
+ end
18
+
19
+ # At this stage, we only care that the servlet spec says PATH_INFO is decoded so special case
20
+ # it. There might be others needed, but webrick and thin return an encoded PATH_INFO so this'll
21
+ # do for now.
22
+ # http://bulknews.typepad.com/blog/2009/09/path_info-decoding-horrors.html
23
+ # http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/http/HttpServletRequest.html#getPathInfo%28%29
24
+ def raw_path_info
25
+ ((servlet_request = @env['java.servlet_request']) &&
26
+ raw_path_info_from_servlet_request(servlet_request)) || path_info
27
+ end
28
+
29
+ def get_or_head?
30
+ method = @env["REQUEST_METHOD"]
31
+ method == "GET" || method == "HEAD"
32
+ end
33
+
34
+ def options?
35
+ @env["REQUEST_METHOD"] == 'OPTIONS'
36
+ end
37
+
38
+ def entity
39
+ return @entity if defined?(@entity)
40
+ @entity = if media_type
41
+ media_type.new_entity(
42
+ :binary_data_stream => env['rack.input'],
43
+ :binary_data_length => content_length && content_length.to_i,
44
+ :encoding => content_charset,
45
+ :media_type_params => media_type_params
46
+ )
47
+ end
48
+ end
49
+
50
+ def media_type
51
+ @mt ||= (mt = super and Doze::MediaType[mt])
52
+ end
53
+
54
+ # For now, to do authentication you need some (rack) middleware that sets one of these env's.
55
+ # See :session_from_rack_env under Doze::Application config
56
+ def session
57
+ @session ||= @app.config[:session_from_rack_env].call(@env)
58
+ end
59
+
60
+ def session_authenticated?
61
+ @session_authenticated ||= (session && @app.config[:session_authenticated].call(session))
62
+ end
63
+
64
+ EXTENSION_REGEXP = /\.([a-z0-9\-_]+)$/
65
+
66
+ # splits the raw_path_info into a routing path, and an optional file extension for use with
67
+ # special media-type-specific file extension handling (where :media_type_extensions => true
68
+ # configured on the app)
69
+ def routing_path_and_extension
70
+ @routing_path_and_extension ||= begin
71
+ path = raw_path_info
72
+ extension = nil
73
+
74
+ if @app.config[:media_type_extensions] && (match = EXTENSION_REGEXP.match(path))
75
+ path = match.pre_match
76
+ extension = match[1]
77
+ end
78
+
79
+ [path, extension]
80
+ end
81
+ end
82
+
83
+ def routing_path
84
+ routing_path_and_extension[0]
85
+ end
86
+
87
+ def extension
88
+ routing_path_and_extension[1]
89
+ end
90
+
91
+ # Makes a Doze::Negotiator to do content negotiation on behalf of this request
92
+ def negotiator(ignore_unacceptable_accepts=false)
93
+ Doze::Negotiator.new(self, ignore_unacceptable_accepts)
94
+ end
95
+
96
+ private
97
+
98
+ URL_CHUNK = /^\/[^\/\?]+/
99
+ URL_UP_TO_URI = /^(\w)+:\/\/[\w0-9\-\.]+(:[0-9]+)?/
100
+ URL_SEARCHPART = /\?.+$/
101
+
102
+ # FIXME - This doesn't do anything with the script name, which will cause trouble
103
+ # if one is specified
104
+ def raw_path_info_from_servlet_request(servlet_request)
105
+ # servlet spec decodes the path info, we want an unencoded version
106
+ # fortunately getRequestURL is unencoded, but includes extra stuff - chop it off
107
+ sb = servlet_request.getRequestURL.toString
108
+ # chomp off the proto, host and optional port
109
+ sb = sb.gsub(URL_UP_TO_URI, "")
110
+
111
+ # chop off context path if one is specified - not sure if this is desired behaviour
112
+ # but conforms to servlet spec and then remove the search part
113
+ if servlet_request.getContextPath == ""
114
+ sb
115
+ else
116
+ sb.gsub(URL_CHUNK, "")
117
+ end.gsub(URL_SEARCHPART, "")
118
+ end
119
+ end
@@ -0,0 +1,21 @@
1
+ # A special resource class used to represent errors which are available in different media_types etc.
2
+ # Used by the framework to render 5xx / 4xx etc
3
+ #
4
+ # The framework supplies this, based on Doze::Serialization::Resource, as the default implementation
5
+ # but you may specify your own error resource class in the app config.
6
+ require 'doze/resource'
7
+ require 'doze/serialization/resource'
8
+ class Doze::Resource::Error
9
+ include Doze::Resource
10
+ include Doze::Serialization::Resource
11
+
12
+ def initialize(status=Doze::Utils::STATUS_INTERNAL_SERVER_ERROR, message=Rack::Utils::HTTP_STATUS_CODES[status], extras={})
13
+ @status = status
14
+ @message = message
15
+ @extra_properties = extras
16
+ end
17
+
18
+ def get_data
19
+ @extra_properties.merge(:status => @status, :message => @message)
20
+ end
21
+ end