rets-hack 0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/Manifest.txt +58 -0
  4. data/README.md +129 -0
  5. data/Rakefile +28 -0
  6. data/bin/rets +202 -0
  7. data/example/connect.rb +19 -0
  8. data/example/get-photos.rb +20 -0
  9. data/example/get-property.rb +16 -0
  10. data/lib/rets/client.rb +373 -0
  11. data/lib/rets/client_progress_reporter.rb +48 -0
  12. data/lib/rets/http_client.rb +133 -0
  13. data/lib/rets/locking_http_client.rb +34 -0
  14. data/lib/rets/measuring_http_client.rb +27 -0
  15. data/lib/rets/metadata/caching.rb +59 -0
  16. data/lib/rets/metadata/containers.rb +89 -0
  17. data/lib/rets/metadata/file_cache.rb +29 -0
  18. data/lib/rets/metadata/json_serializer.rb +27 -0
  19. data/lib/rets/metadata/lookup_table.rb +65 -0
  20. data/lib/rets/metadata/lookup_type.rb +19 -0
  21. data/lib/rets/metadata/marshal_serializer.rb +27 -0
  22. data/lib/rets/metadata/multi_lookup_table.rb +70 -0
  23. data/lib/rets/metadata/null_cache.rb +24 -0
  24. data/lib/rets/metadata/resource.rb +103 -0
  25. data/lib/rets/metadata/rets_class.rb +57 -0
  26. data/lib/rets/metadata/rets_object.rb +41 -0
  27. data/lib/rets/metadata/root.rb +155 -0
  28. data/lib/rets/metadata/table.rb +33 -0
  29. data/lib/rets/metadata/table_factory.rb +19 -0
  30. data/lib/rets/metadata/yaml_serializer.rb +27 -0
  31. data/lib/rets/metadata.rb +18 -0
  32. data/lib/rets/parser/compact.rb +117 -0
  33. data/lib/rets/parser/error_checker.rb +56 -0
  34. data/lib/rets/parser/multipart.rb +39 -0
  35. data/lib/rets.rb +269 -0
  36. data/test/fixtures.rb +324 -0
  37. data/test/helper.rb +14 -0
  38. data/test/test_caching.rb +89 -0
  39. data/test/test_client.rb +307 -0
  40. data/test/test_error_checker.rb +87 -0
  41. data/test/test_file_cache.rb +42 -0
  42. data/test/test_http_client.rb +132 -0
  43. data/test/test_json_serializer.rb +26 -0
  44. data/test/test_locking_http_client.rb +29 -0
  45. data/test/test_marshal_serializer.rb +26 -0
  46. data/test/test_metadata.rb +71 -0
  47. data/test/test_metadata_class.rb +50 -0
  48. data/test/test_metadata_lookup_table.rb +21 -0
  49. data/test/test_metadata_lookup_type.rb +21 -0
  50. data/test/test_metadata_multi_lookup_table.rb +60 -0
  51. data/test/test_metadata_object.rb +33 -0
  52. data/test/test_metadata_resource.rb +148 -0
  53. data/test/test_metadata_root.rb +151 -0
  54. data/test/test_metadata_table.rb +21 -0
  55. data/test/test_metadata_table_factory.rb +24 -0
  56. data/test/test_parser_compact.rb +115 -0
  57. data/test/test_parser_multipart.rb +39 -0
  58. data/test/test_yaml_serializer.rb +26 -0
  59. data/test/vcr_cassettes/unauthorized_response.yml +262 -0
  60. metadata +227 -0
@@ -0,0 +1,373 @@
1
+ require 'logger'
2
+
3
+ module Rets
4
+ class Client
5
+ COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
6
+ CASE_INSENSITIVE_PROC = Proc.new { |h,k| h.key?(k.downcase) ? h[k.downcase] : nil }
7
+
8
+ attr_accessor :cached_metadata, :client_progress, :logger, :login_url, :options
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ clean_setup
13
+ end
14
+
15
+ def clean_setup
16
+ if options.fetch(:login_after_error, true)
17
+ @capabilities = nil
18
+ end
19
+ @metadata = nil
20
+ @tries = nil
21
+ @login_url = options[:login_url]
22
+ @cached_metadata = options[:metadata]
23
+ @cached_capabilities = options[:capabilities]
24
+ @logger = options[:logger] || FakeLogger.new
25
+ @client_progress = ClientProgressReporter.new(logger, options[:stats_collector], options[:stats_prefix])
26
+ @http_client = Rets::HttpClient.from_options(options, logger)
27
+ @caching = Metadata::Caching.make(options)
28
+ end
29
+
30
+ # Attempts to login by making an empty request to the URL provided in
31
+ # initialize. Returns the capabilities that the RETS server provides, per
32
+ # page 34 of http://www.realtor.org/retsorg.nsf/retsproto1.7d6.pdf#page=34
33
+ def login
34
+ res = http_get(login_url)
35
+ Parser::ErrorChecker.check(res)
36
+
37
+ new_capabilities = extract_capabilities(Nokogiri.parse(res.body))
38
+ unless new_capabilities
39
+ raise UnknownResponse, "Cannot read rets server capabilities."
40
+ end
41
+ @capabilities = new_capabilities
42
+ end
43
+
44
+ def logout
45
+ unless capabilities["Logout"]
46
+ raise NoLogout.new('No logout method found for rets client')
47
+ end
48
+ http_get(capability_url("Logout"))
49
+ rescue UnknownResponse => e
50
+ unless e.message.match(/expected a 200, but got 401/)
51
+ raise e
52
+ end
53
+ end
54
+
55
+ # Finds records.
56
+ #
57
+ # [quantity] Return the first record, or an array of records.
58
+ # Uses a symbol <tt>:first</tt> or <tt>:all</tt>, respectively.
59
+ #
60
+ # [opts] A hash of arguments used to construct the search query,
61
+ # using the following keys:
62
+ #
63
+ # <tt>:search_type</tt>:: Required. The resource to search for.
64
+ # <tt>:class</tt>:: Required. The class of the resource to search for.
65
+ # <tt>:query</tt>:: Required. The DMQL2 query string to execute.
66
+ # <tt>:limit</tt>:: The number of records to request from the server.
67
+ # <tt>:resolve</tt>:: Provide resolved values that use metadata instead
68
+ # of raw system values.
69
+ #
70
+ # Any other keys are converted to the RETS query format, and passed
71
+ # to the server as part of the query. For instance, the key <tt>:offset</tt>
72
+ # will be sent as +Offset+.
73
+ #
74
+ def find(quantity, opts = {})
75
+ case quantity
76
+ when :first then find_with_retries(opts.merge(:limit => 1)).first
77
+ when :all then find_with_retries(opts)
78
+ else raise ArgumentError, "First argument must be :first or :all"
79
+ end
80
+ end
81
+
82
+ alias search find
83
+
84
+ def find_with_retries(opts = {})
85
+ retries = 0
86
+ find_with_given_retry(retries, opts)
87
+ end
88
+
89
+ def find_with_given_retry(retries, opts)
90
+ begin
91
+ find_every(opts)
92
+ rescue NoRecordsFound => e
93
+ if opts.fetch(:no_records_not_an_error, false)
94
+ client_progress.no_records_found
95
+ opts[:count] == COUNT.only ? 0 : []
96
+ else
97
+ handle_find_failure(retries, opts, e)
98
+ end
99
+ rescue InvalidRequest, HttpError => e
100
+ handle_find_failure(retries, opts, e)
101
+ rescue AuthorizationFailure => e
102
+ login
103
+ handle_find_failure(retries, opts, e)
104
+ end
105
+ end
106
+
107
+ def handle_find_failure(retries, opts, e)
108
+ max_retries = fetch_max_retries(opts)
109
+ if retries < max_retries
110
+ retries += 1
111
+ wait_before_next_request
112
+ client_progress.find_with_retries_failed_a_retry(e, retries, max_retries)
113
+ clean_setup
114
+ find_with_given_retry(retries, opts)
115
+ else
116
+ client_progress.find_with_retries_exceeded_retry_count(e)
117
+ raise e
118
+ end
119
+ end
120
+
121
+ def fetch_max_retries(hash)
122
+ hash[:max_retries] || options.fetch(:max_retries, 3)
123
+ end
124
+
125
+ def wait_before_next_request
126
+ sleep_time = Float(options.fetch(:recoverable_error_wait_secs, 0))
127
+ if sleep_time > 0
128
+ logger.info "Waiting #{sleep_time} seconds before next attempt"
129
+ sleep sleep_time
130
+ end
131
+ end
132
+
133
+ def find_every(opts)
134
+ raise ArgumentError.new("missing option :search_type (provide the name of a RETS resource)") unless opts[:search_type]
135
+ raise ArgumentError.new("missing option :class (provide the name of a RETS class)") unless opts[:class]
136
+
137
+ params = {
138
+ "SearchType" => opts.fetch(:search_type),
139
+ "Class" => opts.fetch(:class),
140
+ "Count" => opts[:count],
141
+ "Format" => opts.fetch(:format, "COMPACT"),
142
+ "Limit" => opts[:limit],
143
+ "Offset" => opts[:offset],
144
+ "Select" => opts[:select],
145
+ "RestrictedIndicator" => opts[:RestrictedIndicator],
146
+ "StandardNames" => opts[:standard_name],
147
+ "Payload" => opts[:payload],
148
+ "Query" => opts[:query],
149
+ "QueryType" => opts.fetch(:query_type, "DMQL2"),
150
+ }.reject { |k,v| v.nil? }
151
+ res = clean_response(http_post(capability_url("Search"), params))
152
+
153
+ if opts[:count] == COUNT.only
154
+ Parser::Compact.get_count(res.body)
155
+ else
156
+ results = Parser::Compact.parse_document(
157
+ res.body
158
+ )
159
+ if opts[:resolve]
160
+ rets_class = find_rets_class(opts[:search_type], opts[:class])
161
+ decorate_results(results, rets_class)
162
+ else
163
+ results
164
+ end
165
+ end
166
+ end
167
+
168
+ def find_rets_class(resource_name, rets_class_name)
169
+ metadata.tree[resource_name].find_rets_class(rets_class_name)
170
+ end
171
+
172
+ def decorate_results(results, rets_class)
173
+ results.map do |result|
174
+ decorate_result(result, rets_class)
175
+ end
176
+ end
177
+
178
+ def decorate_result(result, rets_class)
179
+ result.each do |key, value|
180
+ table = rets_class.find_table(key)
181
+ if table
182
+ result[key] = table.resolve(value.to_s)
183
+ else
184
+ #can't resolve just leave the value be
185
+ client_progress.could_not_resolve_find_metadata(key)
186
+ end
187
+ end
188
+ end
189
+
190
+ # Returns an array of all objects associated with the given resource.
191
+ def all_objects(opts = {})
192
+ objects("*", opts)
193
+ end
194
+
195
+ # Returns an array of specified objects.
196
+ def objects(object_ids, opts = {})
197
+ response = case object_ids
198
+ when String then fetch_object(object_ids, opts)
199
+ when Array then fetch_object(object_ids.join(":"), opts)
200
+ else raise ArgumentError, "Expected instance of String or Array, but got #{object_ids.inspect}."
201
+ end
202
+
203
+ create_parts_from_response(response)
204
+ end
205
+
206
+ def create_parts_from_response(response)
207
+ content_type = response.header["content-type"][0]
208
+
209
+ if content_type.nil?
210
+ raise MalformedResponse, "Unable to read content-type from response: #{response.inspect}"
211
+ end
212
+
213
+ if content_type.include?("multipart")
214
+ boundary = content_type.scan(/boundary="?([^;"]*)?/).join
215
+
216
+ parts = Parser::Multipart.parse(response.body, boundary)
217
+
218
+ logger.debug "Rets::Client: Found #{parts.size} parts"
219
+
220
+ return parts
221
+ else
222
+ logger.debug "Rets::Client: Found 1 part (the whole body)"
223
+
224
+ # fake a multipart for interface compatibility
225
+ headers = {}
226
+ response.headers.each { |k,v| headers[k.downcase] = v }
227
+ part = Parser::Multipart::Part.new(headers, response.body)
228
+
229
+ return [part]
230
+ end
231
+ end
232
+
233
+ # Returns a single object.
234
+ #
235
+ # resource RETS resource as defined in the resource metadata.
236
+ # object_type an object type defined in the object metadata.
237
+ # resource_id the KeyField value of the given resource instance.
238
+ # object_id can be "*" or a colon delimited string of integers or an array of integers.
239
+ def object(object_id, opts = {})
240
+ response = fetch_object(Array(object_id).join(':'), opts)
241
+ response.body
242
+ end
243
+
244
+ def fetch_object(object_id, opts = {})
245
+ params = {
246
+ "Resource" => opts.fetch("resource"),
247
+ "Type" => opts.fetch("object_type"),
248
+ "ID" => "#{opts.fetch("resource_id")}:#{object_id}",
249
+ "Location" => opts.fetch("location", 0)
250
+ }
251
+
252
+ extra_headers = {
253
+ "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
254
+ }
255
+
256
+ http_post(capability_url("GetObject"), params, extra_headers)
257
+ end
258
+
259
+ def metadata(types=nil)
260
+ return @metadata if @metadata
261
+ @cached_metadata ||= @caching.load(@logger)
262
+ if cached_metadata && (options[:skip_metadata_uptodate_check] ||
263
+ cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
264
+ client_progress.use_cached_metadata
265
+ @metadata = cached_metadata
266
+ else
267
+ client_progress.bad_cached_metadata(cached_metadata)
268
+ @metadata = Metadata::Root.new(logger, retrieve_metadata(types))
269
+ @caching.save(metadata)
270
+ end
271
+ @metadata
272
+ end
273
+
274
+ def retrieve_metadata(types=nil)
275
+ raw_metadata = {}
276
+ (types || Metadata::METADATA_TYPES).each {|type|
277
+ raw_metadata[type] = retrieve_metadata_type(type)
278
+ }
279
+ raw_metadata
280
+ end
281
+
282
+ def retrieve_metadata_type(type)
283
+ res = http_post(capability_url("GetMetadata"),
284
+ { "Format" => "COMPACT",
285
+ "Type" => "METADATA-#{type}",
286
+ "ID" => "0"
287
+ })
288
+ clean_response(res).body
289
+ end
290
+
291
+ # The capabilies as provided by the RETS server during login.
292
+ #
293
+ # Currently, only the path in the endpoint URLs is used[1]. Host,
294
+ # port, other details remaining constant with those provided to
295
+ # the constructor.
296
+ #
297
+ # [1] In fact, sometimes only a path is returned from the server.
298
+ def capabilities
299
+ if @capabilities
300
+ @capabilities
301
+ elsif @cached_capabilities
302
+ @capabilities = add_case_insensitive_default_proc(@cached_capabilities)
303
+ else
304
+ login
305
+ end
306
+ end
307
+
308
+ def capability_url(name)
309
+ val = capabilities[name] || capabilities[name.downcase]
310
+
311
+ raise UnknownCapability.new(name, capabilities.keys) unless val
312
+
313
+ begin
314
+ if val.downcase.match(/^https?:\/\//)
315
+ uri = URI.parse(val)
316
+ else
317
+ uri = URI.parse(login_url)
318
+ uri.path = val
319
+ end
320
+ rescue URI::InvalidURIError
321
+ raise MalformedResponse, "Unable to parse capability URL: #{name} => #{val.inspect}"
322
+ end
323
+ uri.to_s
324
+ end
325
+
326
+ def extract_capabilities(document)
327
+ raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip
328
+
329
+ # ... :(
330
+ # Feel free to make this better. It has a test.
331
+ hash = raw_key_values.split(/\n/).
332
+ map { |r| r.split(/\=/, 2) }.
333
+ each_with_object({}) { |(k,v), h| h[k.strip.downcase] = v.strip }
334
+
335
+ add_case_insensitive_default_proc(hash)
336
+ end
337
+
338
+ def add_case_insensitive_default_proc(hash)
339
+ new_hash = hash.dup
340
+ new_hash.default_proc = CASE_INSENSITIVE_PROC
341
+ new_hash
342
+ end
343
+
344
+ def save_cookie_store
345
+ @http_client.save_cookie_store
346
+ end
347
+
348
+ def http_get(url, params=nil, extra_headers={})
349
+ clean_response(@http_client.http_get(url, params, extra_headers))
350
+ end
351
+
352
+ def clean_response(res)
353
+ res.body.encode!("UTF-8", res.body.encoding, :invalid => :replace, :undef => :replace)
354
+ res
355
+ end
356
+
357
+ def http_post(url, params, extra_headers = {})
358
+ @http_client.http_post(url, params, extra_headers)
359
+ end
360
+
361
+ def tries
362
+ @tries ||= 1
363
+
364
+ (@tries += 1) - 1
365
+ end
366
+
367
+ class FakeLogger < Logger
368
+ def initialize
369
+ super(IO::NULL)
370
+ end
371
+ end
372
+ end
373
+ end
@@ -0,0 +1,48 @@
1
+ module Rets
2
+ class NullStatsReporter
3
+ def time(metric_name, &block)
4
+ block.call
5
+ end
6
+
7
+ def gauge(metric_name, measurement)
8
+ end
9
+
10
+ def count(metric_name, count=1)
11
+ end
12
+ end
13
+
14
+ class ClientProgressReporter
15
+ def initialize(logger, stats, stats_prefix)
16
+ @logger = logger
17
+ @stats = stats || NullStatsReporter.new
18
+ @stats_prefix = stats_prefix
19
+ end
20
+
21
+ def find_with_retries_failed_a_retry(exception, retries, max_retries)
22
+ @stats.count("#{@stats_prefix}find_with_retries_failed_retry")
23
+ @logger.warn("Rets::Client: Failed with message: #{exception.message}")
24
+ @logger.info("Rets::Client: Retry #{retries}/#{max_retries}")
25
+ end
26
+
27
+ def find_with_retries_exceeded_retry_count(exception)
28
+ @stats.count("#{@stats_prefix}find_with_retries_exceeded_retry_count")
29
+ end
30
+
31
+ def no_records_found
32
+ @logger.info("Rets::Client: No Records Found")
33
+ end
34
+
35
+ def could_not_resolve_find_metadata(key)
36
+ @stats.count("#{@stats_prefix}could_not_resolve_find_metadata")
37
+ @logger.warn "Rets::Client: Can't resolve find metadata for #{key.inspect}"
38
+ end
39
+
40
+ def use_cached_metadata
41
+ @logger.info "Rets::Client: Use cached metadata"
42
+ end
43
+
44
+ def bad_cached_metadata(cached_metadata)
45
+ @logger.info cached_metadata ? "Rets::Client: Cached metadata out of date" : "Rets::Client: Cached metadata unavailable"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,133 @@
1
+ require 'http-cookie'
2
+ require 'httpclient'
3
+
4
+ module Rets
5
+ class HttpClient
6
+ attr_reader :http, :options, :logger, :login_url
7
+
8
+ def initialize(http, options, logger, login_url)
9
+ @http = http
10
+ @options = options
11
+ @logger = logger
12
+ @login_url = login_url
13
+ @options.fetch(:ca_certs, []).each {|c| @http.ssl_config.add_trust_ca(c) }
14
+ end
15
+
16
+ def self.from_options(options, logger)
17
+ if options[:http_proxy]
18
+ http = HTTPClient.new(options.fetch(:http_proxy))
19
+
20
+ if options[:proxy_username]
21
+ http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
22
+ end
23
+ else
24
+ http = HTTPClient.new
25
+ end
26
+
27
+ if options[:receive_timeout]
28
+ http.receive_timeout = options[:receive_timeout]
29
+ end
30
+
31
+ if options[:cookie_store]
32
+ ensure_cookie_store_exists! options[:cookie_store]
33
+ http.set_cookie_store(options[:cookie_store])
34
+ end
35
+
36
+ http_client = new(http, options, logger, options[:login_url])
37
+
38
+ if options[:http_timing_stats_collector]
39
+ http_client = Rets::MeasuringHttpClient.new(http_client, options.fetch(:http_timing_stats_collector), options.fetch(:http_timing_stats_prefix))
40
+ end
41
+
42
+ if options[:lock_around_http_requests]
43
+ http_client = Rets::LockingHttpClient.new(http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
44
+ end
45
+
46
+ http_client
47
+ end
48
+
49
+ def self.ensure_cookie_store_exists!(cookie_store)
50
+ unless File.exist? cookie_store
51
+ FileUtils.touch(cookie_store)
52
+ end
53
+ end
54
+
55
+ def http_get(url, params=nil, extra_headers={})
56
+ http.set_auth(url, options[:username], options[:password])
57
+ headers = extra_headers.merge(rets_extra_headers)
58
+ res = nil
59
+ log_http_traffic("GET", url, params, headers) do
60
+ res = http.get(url, params, headers)
61
+ end
62
+ Parser::ErrorChecker.check(res)
63
+ res
64
+ end
65
+
66
+ def http_post(url, params, extra_headers = {})
67
+ http.set_auth(url, options[:username], options[:password])
68
+ headers = extra_headers.merge(rets_extra_headers)
69
+ res = nil
70
+ log_http_traffic("POST", url, params, headers) do
71
+ res = http.post(url, params, headers)
72
+ end
73
+ Parser::ErrorChecker.check(res)
74
+ res
75
+ end
76
+
77
+ def log_http_traffic(method, url, params, headers, &block)
78
+ # optimization, we don't want to compute log params
79
+ # if logging is off
80
+ if logger.debug?
81
+ logger.debug "Rets::Client >> #{method} #{url}"
82
+ logger.debug "Rets::Client >> params = #{params.inspect}"
83
+ logger.debug "Rets::Client >> headers = #{headers.inspect}"
84
+ end
85
+
86
+ res = block.call
87
+
88
+ # optimization, we don't want to compute log params
89
+ # if logging is off, especially when there is a loop just
90
+ # for logging
91
+ if logger.debug?
92
+ logger.debug "Rets::Client << Status #{res.status_code}"
93
+ res.headers.each { |k, v| logger.debug "Rets::Client << #{k}: #{v}" }
94
+ end
95
+ end
96
+
97
+ def save_cookie_store
98
+ if options[:cookie_store]
99
+ #save session cookies
100
+ @http.cookie_manager.save_cookies(true)
101
+ end
102
+ end
103
+
104
+ def rets_extra_headers
105
+ user_agent = options[:agent] || "Client/1.0"
106
+ rets_version = options[:version] || "RETS/1.7.2"
107
+
108
+ headers = {
109
+ "User-Agent" => user_agent,
110
+ "RETS-Version" => rets_version
111
+ }
112
+
113
+ if options[:ua_password]
114
+ up = Digest::MD5.hexdigest "#{user_agent}:#{options[:ua_password]}"
115
+ session_id = http_cookie('RETS-Session-ID') || ''
116
+ digest = Digest::MD5.hexdigest "#{up}::#{session_id}:#{rets_version}"
117
+ headers.merge!("RETS-UA-Authorization" => "Digest #{digest}")
118
+ end
119
+
120
+ headers
121
+ end
122
+
123
+ def http_cookie(name)
124
+ @http.cookie_manager.cookies(login_url).each do |c|
125
+ if c.name.downcase == name.downcase
126
+ return c.value
127
+ end
128
+ end
129
+
130
+ nil
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,34 @@
1
+ module Rets
2
+ class LockingHttpClient
3
+ def initialize(http_client, locker, lock_name, options={})
4
+ @http_client = http_client
5
+ @locker = locker
6
+ @lock_name = lock_name
7
+ @options = options
8
+ end
9
+
10
+ def http_get(url, params=nil, extra_headers={})
11
+ lock_around do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ lock_around do
18
+ @http_client.http_post(url, params, extra_headers)
19
+ end
20
+ end
21
+
22
+ def save_cookie_store
23
+ @http_client.save_cookie_store
24
+ end
25
+
26
+ def lock_around(&block)
27
+ result = nil
28
+ @locker.lock(@lock_name, @options) do
29
+ result = block.call
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ module Rets
2
+ class MeasuringHttpClient
3
+ def initialize(http_client, stats, prefix)
4
+ @http_client = http_client
5
+ @stats = stats
6
+ @prefix = prefix
7
+ end
8
+
9
+ def http_get(url, params=nil, extra_headers={})
10
+ @stats.count("#{@prefix}.http_get_rate")
11
+ @stats.time("#{@prefix}.http_get") do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ @stats.count("#{@prefix}.http_post_rate")
18
+ @stats.time("#{@prefix}.http_post") do
19
+ @http_client.http_post(url, params, extra_headers)
20
+ end
21
+ end
22
+
23
+ def save_cookie_store
24
+ @http_client.save_cookie_store
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ module Rets
2
+ module Metadata
3
+
4
+ # Metadata caching.
5
+ # @api internal
6
+ class Caching
7
+
8
+ # Given the options passed to Client#initialize, make an
9
+ # instance. Options:
10
+ #
11
+ # * :metadata_cache - Persistence mechanism. Defaults to
12
+ # NullCache.
13
+ #
14
+ # * "metadata_serializer - Serialization mechanism. Defaults to
15
+ # MarshalSerializer.
16
+ def self.make(options)
17
+ cache = options.fetch(:metadata_cache) { Metadata::NullCache.new }
18
+ serializer = options.fetch(:metadata_serializer) do
19
+ Metadata::MarshalSerializer.new
20
+ end
21
+ new(cache, serializer)
22
+ end
23
+
24
+ attr_reader :cache
25
+ attr_reader :serializer
26
+
27
+ # The cache is responsible for reading and writing the
28
+ # serialized metadata. The cache should quack like a
29
+ # Rets::Metadata::FileCache.
30
+ #
31
+ # The serializer is responsible for serializing/deserializing
32
+ # the metadata. The serializer should quack like a
33
+ # Rets::Metadata::MarshalSerializer.
34
+ def initialize(cache, serializer)
35
+ @cache = cache
36
+ @serializer = serializer
37
+ end
38
+
39
+ # Load metadata. Returns a Metadata::Root if successful, or nil
40
+ # if it could be loaded for any reason.
41
+ def load(logger)
42
+ sources = @cache.load do |file|
43
+ @serializer.load(file)
44
+ end
45
+ return nil unless sources.is_a?(Hash)
46
+ Metadata::Root.new(logger, sources)
47
+ end
48
+
49
+ # Save metadata.
50
+ def save(metadata)
51
+ @cache.save do |file|
52
+ @serializer.save(file, metadata.sources)
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end