paul-resourceful 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/Manifest.txt +34 -0
- data/README.markdown +86 -0
- data/Rakefile +14 -0
- data/lib/resourceful.rb +29 -0
- data/lib/resourceful/authentication_manager.rb +107 -0
- data/lib/resourceful/cache_manager.rb +174 -0
- data/lib/resourceful/header.rb +31 -0
- data/lib/resourceful/http_accessor.rb +85 -0
- data/lib/resourceful/net_http_adapter.rb +60 -0
- data/lib/resourceful/options_interpreter.rb +78 -0
- data/lib/resourceful/request.rb +63 -0
- data/lib/resourceful/resource.rb +266 -0
- data/lib/resourceful/response.rb +175 -0
- data/lib/resourceful/stubbed_resource_proxy.rb +47 -0
- data/lib/resourceful/util.rb +6 -0
- data/lib/resourceful/version.rb +1 -0
- data/resourceful.gemspec +30 -0
- data/spec/acceptance_shared_specs.rb +49 -0
- data/spec/acceptance_spec.rb +408 -0
- data/spec/resourceful/authentication_manager_spec.rb +249 -0
- data/spec/resourceful/cache_manager_spec.rb +211 -0
- data/spec/resourceful/header_spec.rb +38 -0
- data/spec/resourceful/http_accessor_spec.rb +125 -0
- data/spec/resourceful/net_http_adapter_spec.rb +96 -0
- data/spec/resourceful/options_interpreter_spec.rb +94 -0
- data/spec/resourceful/request_spec.rb +186 -0
- data/spec/resourceful/resource_spec.rb +600 -0
- data/spec/resourceful/response_spec.rb +238 -0
- data/spec/resourceful/stubbed_resource_proxy_spec.rb +58 -0
- data/spec/simple_http_server_shared_spec.rb +160 -0
- data/spec/simple_http_server_shared_spec_spec.rb +212 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +14 -0
- metadata +98 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# A case-normalizing Hash, adjusting on [] and []=.
|
2
|
+
# Shamelessly swiped from Rack
|
3
|
+
module Resourceful
|
4
|
+
class Header < Hash
|
5
|
+
def initialize(hash={})
|
6
|
+
hash.each { |k, v| self[k] = v }
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_hash
|
10
|
+
{}.replace(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](k)
|
14
|
+
super capitalize(k)
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(k, v)
|
18
|
+
super capitalize(k), v
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_key?(k)
|
22
|
+
super capitalize(k)
|
23
|
+
end
|
24
|
+
|
25
|
+
def capitalize(k)
|
26
|
+
k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }.gsub('_', '-')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
require 'resourceful/version'
|
4
|
+
require 'resourceful/options_interpreter'
|
5
|
+
require 'resourceful/authentication_manager'
|
6
|
+
require 'resourceful/cache_manager'
|
7
|
+
require 'resourceful/resource'
|
8
|
+
require 'resourceful/stubbed_resource_proxy'
|
9
|
+
|
10
|
+
module Resourceful
|
11
|
+
# This is an imitation Logger used when no real logger is
|
12
|
+
# registered. This allows most of the code to assume that there
|
13
|
+
# is always a logger available, which significantly improved the
|
14
|
+
# readability of the logging related code.
|
15
|
+
class BitBucketLogger
|
16
|
+
def warn(*args); end
|
17
|
+
def info(*args); end
|
18
|
+
def debug(*args); end
|
19
|
+
end
|
20
|
+
|
21
|
+
# This is the simplest logger. It just writes everything to STDOUT.
|
22
|
+
class StdOutLogger
|
23
|
+
def warn(*args); puts args; end
|
24
|
+
def info(*args); puts args; end
|
25
|
+
def debug(*args); puts args; end
|
26
|
+
end
|
27
|
+
|
28
|
+
# This class provides a simple interface to the functionality
|
29
|
+
# provided by the Resourceful library. Conceptually this object
|
30
|
+
# acts a collection of all the resources available via HTTP.
|
31
|
+
class HttpAccessor
|
32
|
+
RESOURCEFUL_USER_AGENT_TOKEN = "Resourceful/#{RESOURCEFUL_VERSION}(Ruby/#{RUBY_VERSION})"
|
33
|
+
|
34
|
+
# A logger object to which messages about the activities of this
|
35
|
+
# object will be written. This should be an object that responds
|
36
|
+
# to +#info(message)+ and +#debug(message)+.
|
37
|
+
#
|
38
|
+
# Errors will not be logged. Instead an exception will be raised
|
39
|
+
# and the application code should log it if appropriate.
|
40
|
+
attr_accessor :logger, :cache_manager
|
41
|
+
|
42
|
+
attr_reader :auth_manager
|
43
|
+
|
44
|
+
attr_reader :user_agent_tokens
|
45
|
+
|
46
|
+
INIT_OPTIONS = OptionsInterpreter.new do
|
47
|
+
option(:logger, :default => Resourceful::BitBucketLogger.new)
|
48
|
+
option(:user_agent, :default => []) {|ua| [ua].flatten}
|
49
|
+
option(:cache_manager, :default => NullCacheManager.new)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Initializes a new HttpAccessor. Valid options:
|
53
|
+
#
|
54
|
+
# +:logger+:: A Logger object that the new HTTP accessor should
|
55
|
+
# send log messages
|
56
|
+
#
|
57
|
+
# +:user_agent+:: One or more additional user agent tokens to
|
58
|
+
# added to the user agent string.
|
59
|
+
def initialize(options = {})
|
60
|
+
@user_agent_tokens = [RESOURCEFUL_USER_AGENT_TOKEN]
|
61
|
+
|
62
|
+
INIT_OPTIONS.interpret(options) do |opts|
|
63
|
+
@user_agent_tokens.push(*opts[:user_agent].reverse)
|
64
|
+
self.logger = opts[:logger]
|
65
|
+
@auth_manager = AuthenticationManager.new()
|
66
|
+
@cache_manager = opts[:cache_manager]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the string that identifies this HTTP accessor. If you
|
71
|
+
# want to add a token to the user agent string simply add the new
|
72
|
+
# token to the end of +#user_agent_tokens+.
|
73
|
+
def user_agent_string
|
74
|
+
user_agent_tokens.reverse.join(' ')
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a resource object representing the resource indicated
|
78
|
+
# by the specified URI. A resource object will be created if necessary.
|
79
|
+
def resource(uri, opts = {})
|
80
|
+
resource = Resource.new(self, uri, opts)
|
81
|
+
end
|
82
|
+
alias [] resource
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
require 'pathname'
|
6
|
+
require Pathname(__FILE__).dirname + 'header'
|
7
|
+
|
8
|
+
module Addressable
|
9
|
+
class URI
|
10
|
+
def absolute_path
|
11
|
+
absolute_path = ""
|
12
|
+
absolute_path << self.path.to_s
|
13
|
+
absolute_path << "?#{self.query}" if self.query != nil
|
14
|
+
absolute_path << "##{self.fragment}" if self.fragment != nil
|
15
|
+
return absolute_path
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Resourceful
|
21
|
+
|
22
|
+
class NetHttpAdapter
|
23
|
+
def self.make_request(method, uri, body = nil, header = nil)
|
24
|
+
uri = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri)
|
25
|
+
|
26
|
+
req = net_http_request_class(method).new(uri.absolute_path)
|
27
|
+
header.each { |k,v| req[k] = v } if header
|
28
|
+
conn = Net::HTTP.new(uri.host, uri.port)
|
29
|
+
conn.use_ssl = (/https/i === uri.scheme)
|
30
|
+
begin
|
31
|
+
conn.start
|
32
|
+
res = conn.request(req, body)
|
33
|
+
ensure
|
34
|
+
conn.finish
|
35
|
+
end
|
36
|
+
|
37
|
+
[ Integer(res.code),
|
38
|
+
Resourceful::Header.new(res.header.to_hash),
|
39
|
+
res.body
|
40
|
+
]
|
41
|
+
ensure
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def self.net_http_request_class(method)
|
48
|
+
case method
|
49
|
+
when :get then Net::HTTP::Get
|
50
|
+
when :head then Net::HTTP::Head
|
51
|
+
when :post then Net::HTTP::Post
|
52
|
+
when :put then Net::HTTP::Put
|
53
|
+
when :delete then Net::HTTP::Delete
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Resourceful
|
4
|
+
# Class that supports a declarative way to pick apart an options
|
5
|
+
# hash.
|
6
|
+
#
|
7
|
+
# OptionsInterpreter.new do
|
8
|
+
# option(:accept) { |accept| [accept].flatten.map{|m| m.to_str} }
|
9
|
+
# option(:http_header_fields, :default => {})
|
10
|
+
# end.interpret(:accept => 'this/that')
|
11
|
+
# # => {:accept => ['this/that'], :http_header_fields => { } }
|
12
|
+
#
|
13
|
+
# The returned hash contains :accept with the pass accept option
|
14
|
+
# value transformed into an array and :http_header_fields with its
|
15
|
+
# default value.
|
16
|
+
#
|
17
|
+
# OptionsInterpreter.new do
|
18
|
+
# option(:max_redirects)
|
19
|
+
# end.interpret(:foo => 1, :bar => 2)
|
20
|
+
# # Raises ArgumentError: Unrecognized options: foo, bar
|
21
|
+
#
|
22
|
+
# If options are passed that are not defined an exception is raised.
|
23
|
+
#
|
24
|
+
class OptionsInterpreter
|
25
|
+
def self.interpret(options_hash, &block)
|
26
|
+
interpreter = self.new(options_hash)
|
27
|
+
interpreter.instance_eval(&block)
|
28
|
+
|
29
|
+
interpreter.interpret
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(&block)
|
33
|
+
@handlers = Hash.new
|
34
|
+
|
35
|
+
instance_eval(&block) if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
def interpret(options_hash, &block)
|
39
|
+
unless (unrecognized_options = (options_hash.keys - supported_options)).empty?
|
40
|
+
raise ArgumentError, "Unrecognized options: #{unrecognized_options.join(", ")}"
|
41
|
+
end
|
42
|
+
|
43
|
+
options = Hash.new
|
44
|
+
handlers.each do |opt_name, a_handler|
|
45
|
+
opt_val = a_handler.call(options_hash)
|
46
|
+
options[opt_name] = opt_val if opt_val
|
47
|
+
end
|
48
|
+
|
49
|
+
yield(options) if block_given?
|
50
|
+
|
51
|
+
options
|
52
|
+
end
|
53
|
+
|
54
|
+
def option(name, opts = {}, &block)
|
55
|
+
|
56
|
+
passed_value_fetcher = if opts[:default]
|
57
|
+
default_value = opts[:default]
|
58
|
+
lambda{|options_hash| options_hash[name] || default_value}
|
59
|
+
else
|
60
|
+
lambda{|options_hash| options_hash[name]}
|
61
|
+
end
|
62
|
+
|
63
|
+
handlers[name] = if block_given?
|
64
|
+
lambda{|options_hash| (val = passed_value_fetcher.call(options_hash)) ? block.call(val) : nil}
|
65
|
+
else
|
66
|
+
passed_value_fetcher
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def supported_options
|
71
|
+
handlers.keys
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
attr_reader :handlers
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'benchmark'
|
3
|
+
require Pathname(__FILE__).dirname + 'response'
|
4
|
+
require Pathname(__FILE__).dirname + 'net_http_adapter'
|
5
|
+
|
6
|
+
module Resourceful
|
7
|
+
|
8
|
+
class Request
|
9
|
+
|
10
|
+
REDIRECTABLE_METHODS = [:get, :head]
|
11
|
+
|
12
|
+
attr_accessor :method, :resource, :body, :header
|
13
|
+
attr_reader :request_time
|
14
|
+
|
15
|
+
def initialize(http_method, resource, body = nil, header = nil)
|
16
|
+
@method, @resource, @body = http_method, resource, body
|
17
|
+
@header = header.is_a?(Resourceful::Header) ? header : Resourceful::Header.new(header || {})
|
18
|
+
|
19
|
+
@header['Accept-Encoding'] = 'gzip, identity'
|
20
|
+
# 'Host' is a required HTTP/1.1 header, so set it if it isn't already
|
21
|
+
@header['Host'] ||= Addressable::URI.parse(resource.uri).host
|
22
|
+
|
23
|
+
# Setting the date isn't a bad idea, either
|
24
|
+
@header['Date'] ||= Time.now.httpdate
|
25
|
+
end
|
26
|
+
|
27
|
+
def response
|
28
|
+
@request_time = Time.now
|
29
|
+
|
30
|
+
http_resp = NetHttpAdapter.make_request(@method, @resource.uri, @body, @header)
|
31
|
+
response = Resourceful::Response.new(uri, *http_resp)
|
32
|
+
response.request_time = @request_time
|
33
|
+
|
34
|
+
response.authoritative = true
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
def should_be_redirected?
|
39
|
+
if resource.on_redirect.nil?
|
40
|
+
return true if method.in? REDIRECTABLE_METHODS
|
41
|
+
false
|
42
|
+
else
|
43
|
+
resource.on_redirect.call(self, response)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_validation_headers(response)
|
48
|
+
@header['If-None-Match'] = response.header['ETag'] if response.header.has_key?('ETag')
|
49
|
+
@header['If-Modified-Since'] = response.header['Last-Modified'] if response.header.has_key?('Last-Modified')
|
50
|
+
@header['Cache-Control'] = 'max-age=0' if response.header.has_key?('Cache-Control') and response.header['Cache-Control'].include?('must-revalidate')
|
51
|
+
end
|
52
|
+
|
53
|
+
def uri
|
54
|
+
resource.uri
|
55
|
+
end
|
56
|
+
|
57
|
+
def logger
|
58
|
+
resource.logger
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname + 'request'
|
3
|
+
|
4
|
+
module Resourceful
|
5
|
+
|
6
|
+
# This exception used to indicate that the request did not succeed.
|
7
|
+
# The HTTP response is included so that the appropriate actions can
|
8
|
+
# be taken based on the details of that response
|
9
|
+
class UnsuccessfulHttpRequestError < Exception
|
10
|
+
attr_reader :http_response, :http_request
|
11
|
+
|
12
|
+
# Initialize new error from the HTTP request and response attributes.
|
13
|
+
def initialize(http_request, http_response)
|
14
|
+
super("#{http_request.method} request to <#{http_request.uri}> failed with code #{http_response.code}")
|
15
|
+
@http_request = http_request
|
16
|
+
@http_response = http_response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Resource
|
21
|
+
attr_reader :accessor
|
22
|
+
attr_accessor :default_options
|
23
|
+
|
24
|
+
# Build a new resource for a uri
|
25
|
+
#
|
26
|
+
# @param accessor<HttpAccessor>
|
27
|
+
# The parent http accessor
|
28
|
+
# @param uri<String, Addressable::URI>
|
29
|
+
# The uri for the location of the resource
|
30
|
+
def initialize(accessor, uri, options = {})
|
31
|
+
@accessor, @uris = accessor, [uri]
|
32
|
+
@default_options = options
|
33
|
+
@on_redirect = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# The uri used to identify this resource. This is almost always the uri
|
37
|
+
# used to create the resource, but in the case of a permanent redirect, this
|
38
|
+
# will always reflect the lastest uri.
|
39
|
+
#
|
40
|
+
# @return Addressable::URI
|
41
|
+
# The current uri of the resource
|
42
|
+
def effective_uri
|
43
|
+
@uris.first
|
44
|
+
end
|
45
|
+
alias uri effective_uri
|
46
|
+
|
47
|
+
# When performing a redirect, this callback will be executed first. If the callback
|
48
|
+
# returns true, then the redirect is followed, otherwise it is not. The request that
|
49
|
+
# triggered the redirect and the response will be passed into the block. This can be
|
50
|
+
# used to update any links on the client side.
|
51
|
+
#
|
52
|
+
# Example:
|
53
|
+
#
|
54
|
+
# author_resource.on_redirect do |req, resp|
|
55
|
+
# post.author_uri = resp.header['Location']
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# @yieldparam callback<request, response>
|
59
|
+
# The action to be executed when a request results in a redirect. Yields the
|
60
|
+
# current request and result objects to the callback.
|
61
|
+
#
|
62
|
+
# @raise ArgumentError if called without a block
|
63
|
+
def on_redirect(&callback)
|
64
|
+
if block_given?
|
65
|
+
@on_redirect = callback
|
66
|
+
else
|
67
|
+
@on_redirect
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Performs a GET on the resource, following redirects as neccessary, and retriving
|
72
|
+
# it from the local cache if its available and valid.
|
73
|
+
#
|
74
|
+
# @return [Response] The Response to the final request made.
|
75
|
+
#
|
76
|
+
# @raise [UnsuccessfulHttpRequestError] unless the request is a
|
77
|
+
# success, ie the final request returned a 2xx response code
|
78
|
+
def get(header = {})
|
79
|
+
log_request_with_time "GET [#{uri}]" do
|
80
|
+
do_read_request(:get, header)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# :call-seq:
|
85
|
+
# post(data = "", :content_type => mime_type)
|
86
|
+
#
|
87
|
+
# Performs a POST with the given data to the resource, following redirects as
|
88
|
+
# neccessary.
|
89
|
+
#
|
90
|
+
# @param [String] data
|
91
|
+
# The body of the data to be posted
|
92
|
+
# @param [Hash] options
|
93
|
+
# Options to pass into the request header. At the least, :content_type is required.
|
94
|
+
#
|
95
|
+
# @return [Response] The Response to the final request that was made.
|
96
|
+
#
|
97
|
+
# @raise [ArgumentError] unless :content-type is specified in options
|
98
|
+
# @raise [UnsuccessfulHttpRequestError] unless the request is a
|
99
|
+
# success, ie the final request returned a 2xx response code
|
100
|
+
def post(data = "", options = {})
|
101
|
+
raise ArgumentError, ":content_type must be specified" unless options.has_key?(:content_type)
|
102
|
+
|
103
|
+
log_request_with_time "POST [#{uri}]" do
|
104
|
+
do_write_request(:post, data, options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# :call-seq:
|
109
|
+
# put(data = "", :content_type => mime_type)
|
110
|
+
#
|
111
|
+
# Performs a PUT with the given data to the resource, following redirects as
|
112
|
+
# neccessary.
|
113
|
+
#
|
114
|
+
# @param [String] data
|
115
|
+
# The body of the data to be posted
|
116
|
+
# @param [Hash] options
|
117
|
+
# Options to pass into the request header. At the least, :content_type is required.
|
118
|
+
#
|
119
|
+
# @return [Response] The response to the final request made.
|
120
|
+
#
|
121
|
+
# @raise [ArgumentError] unless :content-type is specified in options
|
122
|
+
# @raise [UnsuccessfulHttpRequestError] unless the request is a
|
123
|
+
# success, ie the final request returned a 2xx response code
|
124
|
+
def put(data = "", options = {})
|
125
|
+
raise ArgumentError, ":content_type must be specified" unless options.has_key?(:content_type)
|
126
|
+
|
127
|
+
log_request_with_time "PUT [#{uri}]" do
|
128
|
+
do_write_request(:put, data, options)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Performs a DELETE on the resource, following redirects as neccessary.
|
133
|
+
#
|
134
|
+
# @return <Response>
|
135
|
+
#
|
136
|
+
# @raise [UnsuccessfulHttpRequestError] unless the request is a
|
137
|
+
# success, ie the final request returned a 2xx response code
|
138
|
+
def delete(options = {})
|
139
|
+
log_request_with_time "DELETE [#{uri}]" do
|
140
|
+
do_write_request(:delete, {}, options)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Performs a read request (HEAD, GET). Users should use the #get, etc methods instead.
|
145
|
+
#
|
146
|
+
# This method handles all the work of following redirects.
|
147
|
+
#
|
148
|
+
# @param method<Symbol> The method to perform
|
149
|
+
#
|
150
|
+
# @return <Response>
|
151
|
+
#
|
152
|
+
# @raise [UnsuccessfulHttpRequestError] unless the request is a
|
153
|
+
# success, ie the final request returned a 2xx response code
|
154
|
+
#
|
155
|
+
def do_read_request(method, header = {})
|
156
|
+
request = Resourceful::Request.new(method, self, nil, default_options.merge(header))
|
157
|
+
accessor.auth_manager.add_credentials(request)
|
158
|
+
|
159
|
+
cached_response = accessor.cache_manager.lookup(request)
|
160
|
+
if cached_response
|
161
|
+
logger.info(" Retrieved from cache")
|
162
|
+
if not cached_response.stale?
|
163
|
+
# We're done!
|
164
|
+
return cached_response
|
165
|
+
else
|
166
|
+
logger.info(" Cache entry is stale")
|
167
|
+
request.set_validation_headers(cached_response)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
response = request.response
|
172
|
+
|
173
|
+
if response.is_not_modified?
|
174
|
+
logger.info(" Resource not modified")
|
175
|
+
cached_response.header.merge!(response.header)
|
176
|
+
cached_response.request_time = response.request_time
|
177
|
+
response = cached_response
|
178
|
+
response.authoritative = true
|
179
|
+
end
|
180
|
+
|
181
|
+
if response.is_redirect? and request.should_be_redirected?
|
182
|
+
if response.is_permanent_redirect?
|
183
|
+
@uris.unshift response.header['Location'].first
|
184
|
+
logger.info(" Permanently redirected to #{uri} - Storing new location.")
|
185
|
+
response = do_read_request(method, header)
|
186
|
+
else
|
187
|
+
redirected_resource = Resourceful::Resource.new(self.accessor, response.header['Location'].first)
|
188
|
+
logger.info(" Redirected to #{redirected_resource.uri} - Storing new location.")
|
189
|
+
response = redirected_resource.do_read_request(method, header)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
if response.is_not_authorized? && !@already_tried_with_auth
|
194
|
+
@already_tried_with_auth = true
|
195
|
+
accessor.auth_manager.associate_auth_info(response)
|
196
|
+
logger.info("Authentication Required. Retrying with auth info")
|
197
|
+
response = do_read_request(method, header)
|
198
|
+
end
|
199
|
+
|
200
|
+
raise UnsuccessfulHttpRequestError.new(request,response) unless response.is_success?
|
201
|
+
|
202
|
+
accessor.cache_manager.store(request, response) if response.is_success?
|
203
|
+
|
204
|
+
return response
|
205
|
+
end
|
206
|
+
|
207
|
+
# Performs a write request (POST, PUT, DELETE). Users should use the #post, etc
|
208
|
+
# methods instead.
|
209
|
+
#
|
210
|
+
# This method handles all the work of following redirects.
|
211
|
+
#
|
212
|
+
# @param [Symbol] method The method to perform
|
213
|
+
# @param [String] data Body of the http request.
|
214
|
+
# @param [Hash] header Header for the HTTP resquest.
|
215
|
+
#
|
216
|
+
# @return [Response]
|
217
|
+
#
|
218
|
+
# @raise [UnsuccessfulHttpRequestError] unless the request is a
|
219
|
+
# success, ie the final request returned a 2xx response code
|
220
|
+
def do_write_request(method, data = nil, header = {})
|
221
|
+
request = Resourceful::Request.new(method, self, data, default_options.merge(header))
|
222
|
+
accessor.auth_manager.add_credentials(request)
|
223
|
+
|
224
|
+
response = request.response
|
225
|
+
|
226
|
+
if response.is_redirect? and request.should_be_redirected?
|
227
|
+
if response.is_permanent_redirect?
|
228
|
+
@uris.unshift response.header['Location'].first
|
229
|
+
response = do_write_request(method, data, header)
|
230
|
+
elsif response.code == 303 # see other, must use GET for new location
|
231
|
+
redirected_resource = Resourceful::Resource.new(self.accessor, response.header['Location'].first)
|
232
|
+
response = redirected_resource.do_read_request(:get, header)
|
233
|
+
else
|
234
|
+
redirected_resource = Resourceful::Resource.new(self.accessor, response.header['Location'].first)
|
235
|
+
response = redirected_resource.do_write_request(method, data, header)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if response.is_not_authorized? && !@already_tried_with_auth
|
240
|
+
@already_tried_with_auth = true
|
241
|
+
accessor.auth_manager.associate_auth_info(response)
|
242
|
+
logger.debug("Authentication Required. Retrying with auth info")
|
243
|
+
response = do_write_request(method, data, header)
|
244
|
+
end
|
245
|
+
|
246
|
+
raise UnsuccessfulHttpRequestError.new(request,response) unless response.is_success?
|
247
|
+
|
248
|
+
accessor.cache_manager.invalidate(uri)
|
249
|
+
return response
|
250
|
+
end
|
251
|
+
|
252
|
+
def log_request_with_time(msg, indent = 2)
|
253
|
+
logger.info(" " * indent + msg)
|
254
|
+
result = nil
|
255
|
+
time = Benchmark.measure { result = yield }
|
256
|
+
logger.info(" " * indent + "-> Returned #{result.code} in %.4fs" % time.real)
|
257
|
+
result
|
258
|
+
end
|
259
|
+
|
260
|
+
def logger
|
261
|
+
accessor.logger
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|