rets 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ### 0.5.0 / 2013-09-05
2
+
3
+ * feature: Allow client.count to get integer count
4
+ * feature: Allow for downcased capability names
5
+ * fix: Handle the rets element being empty
6
+ * feature: Instrument rets client with stats reporting
7
+ * feature: Add a locking client
8
+ * feature: Support Basic Authentication
9
+
1
10
  ### 0.4.0 / 2012-08-29
2
11
 
3
12
  * fix: update authentication header to uri matches path
data/Manifest.txt CHANGED
@@ -4,7 +4,6 @@ README.md
4
4
  Rakefile
5
5
  bin/rets
6
6
  lib/rets.rb
7
- lib/rets/authentication.rb
8
7
  lib/rets/client.rb
9
8
  lib/rets/metadata.rb
10
9
  lib/rets/metadata/containers.rb
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'hoe'
3
+ require 'rake/testtask'
3
4
 
4
5
  Hoe.plugin :git, :doofus
5
6
  Hoe.plugin :travis
@@ -8,7 +9,7 @@ Hoe.plugin :gemspec
8
9
  Hoe.spec 'rets' do
9
10
  developer 'Estately, Inc. Open Source', 'opensource@estately.com'
10
11
 
11
- extra_deps << [ "net-http-persistent", "~> 1.7" ]
12
+ extra_deps << [ "httpclient", "~> 2.3.0" ]
12
13
  extra_deps << [ "nokogiri", "~> 1.5.2" ]
13
14
 
14
15
  extra_dev_deps << [ "mocha", "~> 0.11.0" ]
@@ -19,3 +20,8 @@ Hoe.spec 'rets' do
19
20
  self.history_file = 'CHANGELOG.md'
20
21
  self.readme_file = 'README.md'
21
22
  end
23
+
24
+
25
+ Rake::TestTask.new do |t|
26
+ t.pattern = "test/test_*.rb"
27
+ end
data/bin/rets CHANGED
@@ -4,12 +4,14 @@ require "optparse"
4
4
  require "pp"
5
5
 
6
6
  require "rubygems"
7
+
8
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
7
9
  require "rets"
8
10
 
9
11
  class RetsCli
10
12
  def self.parse(args)
11
13
 
12
- actions = %w(metadata search object)
14
+ actions = %w(metadata search count object)
13
15
  options = {:count => 5}
14
16
 
15
17
  opts = OptionParser.new do |opts|
@@ -107,8 +109,6 @@ query = ARGV[1]
107
109
 
108
110
  client = Rets::Client.new(options.merge(:login_url => url))
109
111
 
110
- COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
111
-
112
112
  if options[:capabilities]
113
113
  pp client.capabilities
114
114
  end
@@ -161,9 +161,17 @@ case options[:action]
161
161
  :search_type => options[:resource],
162
162
  :class => options[:class],
163
163
  :query => query,
164
- :count => COUNT.exclude,
164
+ :count => Rets::Client::COUNT.exclude,
165
165
  :limit => options[:limit])
166
166
 
167
+ when "count" then
168
+ pp client.find(:all,
169
+ :search_type => options[:resource],
170
+ :class => options[:class],
171
+ :query => query,
172
+ :count => Rets::Client::COUNT.only,
173
+ :limit => options[:limit])
174
+
167
175
  when "object" then
168
176
 
169
177
  def write_objects(parts)
@@ -191,4 +199,4 @@ case options[:action]
191
199
 
192
200
  end
193
201
 
194
-
202
+ client.logout
data/lib/rets.rb CHANGED
@@ -1,24 +1,45 @@
1
- require 'net/http'
2
1
  require 'uri'
3
- require 'cgi'
4
2
  require 'digest/md5'
5
-
6
- require 'rubygems'
7
- require 'net/http/persistent'
8
3
  require 'nokogiri'
9
4
 
10
5
  module Rets
11
- VERSION = '0.4.0'
6
+ VERSION = '0.5.0'
12
7
 
13
- AuthorizationFailure = Class.new(ArgumentError)
14
- InvalidRequest = Class.new(ArgumentError)
15
8
  MalformedResponse = Class.new(ArgumentError)
16
9
  UnknownResponse = Class.new(ArgumentError)
10
+ NoLogout = Class.new(ArgumentError)
11
+
12
+ class AuthorizationFailure < ArgumentError
13
+ attr_reader :status, :body
14
+ def initialize(status, body)
15
+ @status = status
16
+ @body = body
17
+ super("HTTP status: #{status} (#{body})")
18
+ end
19
+ end
20
+
21
+ class InvalidRequest < ArgumentError
22
+ attr_reader :error_code, :reply_text
23
+ def initialize(error_code, reply_text)
24
+ @error_code = error_code
25
+ @reply_text = reply_text
26
+ super("Got error code #{error_code} (#{reply_text})")
27
+ end
28
+ end
29
+
30
+ class UnknownCapability < ArgumentError
31
+ attr_reader :capability_name
32
+ def initialize(capability_name)
33
+ @capability_name = capability_name
34
+ super("unknown capabilitiy #{capability_name}")
35
+ end
36
+ end
17
37
  end
18
38
 
19
- require 'rets/authentication'
39
+ require 'rets/client'
20
40
  require 'rets/metadata'
21
41
  require 'rets/parser/compact'
22
42
  require 'rets/parser/multipart'
23
-
24
- require 'rets/client'
43
+ require 'rets/measuring_http_client'
44
+ require 'rets/locking_http_client'
45
+ require 'rets/client_progress_reporter'
data/lib/rets/client.rb CHANGED
@@ -1,12 +1,16 @@
1
+ require 'httpclient'
2
+ require 'logger'
3
+ require_relative 'http_client'
4
+
1
5
  module Rets
2
- Session = Struct.new(:auth_digest, :capabilities, :cookies)
6
+ class HttpError < StandardError ; end
3
7
 
4
8
  class Client
5
- DEFAULT_OPTIONS = { :persistent => true }
9
+ DEFAULT_OPTIONS = {}
6
10
 
7
- include Authentication
11
+ COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
8
12
 
9
- attr_accessor :login_uri, :options, :logger, :auth_digest
13
+ attr_accessor :login_url, :options, :logger
10
14
  attr_writer :capabilities, :metadata
11
15
 
12
16
  def initialize(options)
@@ -15,41 +19,47 @@ module Rets
15
19
  end
16
20
 
17
21
  def clean_setup
22
+ self.options = DEFAULT_OPTIONS.merge(@options)
23
+ self.login_url = self.options[:login_url]
18
24
 
19
- @auth_digest = nil
20
25
  @cached_metadata = nil
21
26
  @capabilities = nil
22
- @connection = nil
23
- @cookies = nil
24
27
  @metadata = nil
25
28
  @tries = nil
26
29
  self.capabilities = nil
27
30
 
28
- uri = URI.parse(@options[:login_url])
29
-
30
- uri.user = @options.key?(:username) ? CGI.escape(@options[:username]) : nil
31
- uri.password = @options.key?(:password) ? CGI.escape(@options[:password]) : nil
32
-
33
- self.options = DEFAULT_OPTIONS.merge(@options)
34
- self.login_uri = uri
35
-
36
31
  self.logger = @options[:logger] || FakeLogger.new
37
-
38
- self.session = @options.delete(:session) if @options[:session]
39
- @cached_metadata = @options[:metadata] || nil
32
+ @client_progress = ClientProgressReporter.new(self.logger, options[:stats_collector], options[:stats_prefix])
33
+ @cached_metadata = @options[:metadata]
34
+ @http = HTTPClient.new
35
+ @http.set_cookie_store(options[:cookie_store]) if options[:cookie_store]
36
+
37
+ @http_client = Rets::HttpClient.new(@http, @options, @logger, @login_url)
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
+ if options[:lock_around_http_requests]
42
+ @http_client = Rets::LockingHttpClient.new(@http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
43
+ end
40
44
  end
41
45
 
42
-
43
46
  # Attempts to login by making an empty request to the URL
44
47
  # provided in initialize. Returns the capabilities that the
45
48
  # RETS server provides, per http://retsdoc.onconfluence.com/display/rets172/4.10+Capability+URL+List.
46
49
  def login
47
- response = request(login_uri.path)
48
- self.capabilities = extract_capabilities(Nokogiri.parse(response.body))
50
+ res = http_get(login_url)
51
+ self.capabilities = extract_capabilities(Nokogiri.parse(res.body))
49
52
  raise UnknownResponse, "Cannot read rets server capabilities." unless @capabilities
50
53
  @capabilities
51
54
  end
52
55
 
56
+ def logout
57
+ unless capabilities["Logout"]
58
+ raise NoLogout.new('No logout method found for rets client')
59
+ end
60
+ http_get(capability_url("Logout"))
61
+ end
62
+
53
63
  # Finds records.
54
64
  #
55
65
  # [quantity] Return the first record, or an array of records.
@@ -81,51 +91,41 @@ module Rets
81
91
 
82
92
  def find_with_retries(opts = {})
83
93
  retries = 0
94
+ resolve = opts.delete(:resolve)
84
95
  begin
85
- find_every(opts)
96
+ find_every(opts, resolve)
86
97
  rescue AuthorizationFailure, InvalidRequest => e
87
98
  if retries < 3
88
99
  retries += 1
89
- self.logger.warn "Failed with message: #{e.message}"
90
- self.logger.info "Retry #{retries}/3"
100
+ @client_progress.find_with_retries_failed_a_retry(e, retries)
91
101
  clean_setup
92
102
  retry
93
103
  else
104
+ @client_progress.find_with_retries_exceeded_retry_count(e)
94
105
  raise e
95
106
  end
96
107
  end
97
108
  end
98
109
 
99
- def find_every(opts = {})
100
- search_uri = capability_url("Search")
101
-
102
- resolve = opts.delete(:resolve)
103
-
104
- extras = fixup_keys(opts)
105
-
106
- defaults = {"QueryType" => "DMQL2", "Format" => "COMPACT"}
107
-
108
- query = defaults.merge(extras)
109
-
110
- body = build_key_values(query)
111
-
112
- extra_headers = {
113
- "Content-Type" => "application/x-www-form-urlencoded",
114
- "Content-Length" => body.size.to_s
115
- }
116
-
117
- results = request_with_compact_response(search_uri.path, body, extra_headers)
110
+ def find_every(opts, resolve)
111
+ params = {"QueryType" => "DMQL2", "Format" => "COMPACT"}.merge(fixup_keys(opts))
112
+ res = http_post(capability_url("Search"), params)
118
113
 
119
- if resolve
120
- rets_class = find_rets_class(opts[:search_type], opts[:class])
121
- decorate_results(results, rets_class)
114
+ if opts[:count] == COUNT.only
115
+ Parser::Compact.get_count(res.body)
122
116
  else
123
- results
117
+ results = Parser::Compact.parse_document(res.body)
118
+ if resolve
119
+ rets_class = find_rets_class(opts[:search_type], opts[:class])
120
+ decorate_results(results, rets_class)
121
+ else
122
+ results
123
+ end
124
124
  end
125
125
  end
126
126
 
127
127
  def find_rets_class(resource_name, rets_class_name)
128
- metadata.build_tree[resource_name].find_rets_class(rets_class_name)
128
+ metadata.tree[resource_name].find_rets_class(rets_class_name)
129
129
  end
130
130
 
131
131
  def decorate_results(results, rets_class)
@@ -141,12 +141,11 @@ module Rets
141
141
  result[key] = table.resolve(value.to_s)
142
142
  else
143
143
  #can't resolve just leave the value be
144
- logger.warn "Can't resolve find metadata for #{key.inspect}"
144
+ @client_progress.could_not_resolve_find_metadata(key)
145
145
  end
146
146
  end
147
147
  end
148
148
 
149
-
150
149
  # Returns an array of all objects associated with the given resource.
151
150
  def all_objects(opts = {})
152
151
  objects("*", opts)
@@ -164,7 +163,7 @@ module Rets
164
163
  end
165
164
 
166
165
  def create_parts_from_response(response)
167
- content_type = response["content-type"]
166
+ content_type = response.header["content-type"][0]
168
167
 
169
168
  if content_type.nil?
170
169
  raise MalformedResponse, "Unable to read content-type from response: #{response.inspect}"
@@ -175,13 +174,13 @@ module Rets
175
174
 
176
175
  parts = Parser::Multipart.parse(response.body, boundary)
177
176
 
178
- logger.debug "Found #{parts.size} parts"
177
+ logger.debug "Rets::Client: Found #{parts.size} parts"
179
178
 
180
179
  return parts
181
180
  else
182
181
  # fake a multipart for interface compatibility
183
182
  headers = {}
184
- response.each { |k,v| headers[k] = v }
183
+ response.headers.each { |k,v| headers[k] = v[0] }
185
184
 
186
185
  part = Parser::Multipart::Part.new(headers, response.body)
187
186
 
@@ -194,30 +193,25 @@ module Rets
194
193
  # resource RETS resource as defined in the resource metadata.
195
194
  # object_type an object type defined in the object metadata.
196
195
  # resource_id the KeyField value of the given resource instance.
197
- # object_id can be "*", or a comma delimited string of one or more integers.
196
+ # object_id can be "*" or a colon delimited string of integers or an array of integers.
198
197
  def object(object_id, opts = {})
199
- response = fetch_object(object_id, opts)
200
-
198
+ response = fetch_object(Array(object_id).join(':'), opts)
201
199
  response.body
202
200
  end
203
201
 
204
202
  def fetch_object(object_id, opts = {})
205
- object_uri = capability_url("GetObject")
206
-
207
- body = build_key_values(
208
- "Resource" => opts[:resource],
209
- "Type" => opts[:object_type],
210
- "ID" => "#{opts[:resource_id]}:#{object_id}",
211
- "Location" => opts[:location] || 0
212
- )
203
+ params = {
204
+ "Resource" => opts.fetch(:resource),
205
+ "Type" => opts.fetch(:object_type),
206
+ "ID" => "#{opts.fetch(:resource_id)}:#{object_id}",
207
+ "Location" => opts.fetch(:location, 0)
208
+ }
213
209
 
214
210
  extra_headers = {
215
- "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
216
- "Content-Type" => "application/x-www-form-urlencoded",
217
- "Content-Length" => body.size.to_s
211
+ "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
218
212
  }
219
213
 
220
- request(object_uri.path, body, extra_headers)
214
+ http_post(capability_url("GetObject"), params, extra_headers)
221
215
  end
222
216
 
223
217
  # Changes keys to be camel cased, per the RETS standard for queries.
@@ -238,166 +232,24 @@ module Rets
238
232
 
239
233
  if @cached_metadata && (@options[:skip_metadata_uptodate_check] ||
240
234
  @cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
241
- logger.info "Use cached metadata"
235
+ @client_progress.use_cached_metadata
242
236
  self.metadata = @cached_metadata
243
237
  else
244
- logger.info @cached_metadata ? "Cached metadata out of date" : "Cached metadata unavailable"
238
+ @client_progress.bad_cached_metadata(@cached_metadata)
245
239
  metadata_fetcher = lambda { |type| retrieve_metadata_type(type) }
246
240
  self.metadata = Metadata::Root.new(&metadata_fetcher)
247
241
  end
248
242
  end
249
243
 
250
244
  def retrieve_metadata_type(type)
251
- metadata_uri = capability_url("GetMetadata")
252
-
253
- body = build_key_values(
254
- "Format" => "COMPACT",
255
- "Type" => "METADATA-#{type}",
256
- "ID" => "0"
257
- )
258
-
259
- extra_headers = {
260
- "Content-Type" => "application/x-www-form-urlencoded",
261
- "Content-Length" => body.size.to_s
262
- }
263
-
264
- response = request(metadata_uri.path, body, extra_headers)
265
-
266
- response.body
267
- end
268
-
269
- def raw_request(path, body = nil, extra_headers = {}, &reader)
270
- headers = build_headers(path).merge(extra_headers)
271
-
272
- post = Net::HTTP::Post.new(path, headers)
273
- post.body = body.to_s
274
-
275
- logger.debug <<EOF
276
- >>>> Request
277
- POST #{path}
278
- #{format_headers(headers)}
279
-
280
- #{binary?(body.to_s) ? '<<< BINARY BODY >>>' : body.to_s}
281
- EOF
282
-
283
- connection_args = [Net::HTTP::Persistent === connection ? login_uri : nil, post].compact
284
-
285
- response = connection.request(*connection_args) do |res|
286
- res.read_body(&reader)
287
- end
288
-
289
- logger.debug <<EOF
290
- <<<< Response
291
- #{response.code} #{response.message}
292
- #{format_headers(response.to_hash)}
293
-
294
- #{binary?(response.body.to_s) ? '<<< BINARY BODY >>>' : response.body.to_s}
295
- EOF
296
-
297
- handle_cookies(response)
298
- return response
299
- end
300
-
301
- def digest_auth_request(path, body = nil, extra_headers = {}, &reader)
302
- response = raw_request(path, body, extra_headers, &reader)
303
- if Net::HTTPUnauthorized === response
304
- @auth_digest = extract_digest_header(response)
305
- if @auth_digest
306
- response = raw_request(path, body, extra_headers, &reader)
307
- if Net::HTTPUnauthorized === response
308
- raise AuthorizationFailure, "Authorization failed, check credentials?"
309
- end
310
- end
311
- end
312
- response
313
- end
314
-
315
- def authorization(path)
316
- return nil unless @auth_digest
317
- uri2 = URI.parse(login_uri.to_s)
318
- uri2.user = login_uri.user
319
- uri2.password = login_uri.password
320
- uri2.path = path
321
- build_auth(@auth_digest, uri2, tries)
322
- end
323
-
324
-
325
- def request(*args, &block)
326
- handle_response(digest_auth_request(*args, &block))
327
- end
328
-
329
- def request_with_compact_response(path, body, headers)
330
- response = request(path, body, headers)
331
-
332
- Parser::Compact.parse_document response.body
333
- end
334
-
335
- def extract_digest_header(response)
336
- authenticate_headers = response.get_fields("www-authenticate")
337
- if authenticate_headers
338
- authenticate_headers.detect {|h| h =~ /Digest/}
339
- else
340
- nil
341
- end
342
- end
343
-
344
- def handle_response(response)
345
- if Net::HTTPSuccess === response
346
- ErrorChecker.check(response)
347
- elsif Net::HTTPUnauthorized === response
348
- raise AuthorizationFailure, "Authorization failed, check credentials?"
349
- else
350
- raise UnknownResponse, "Unable to handle response #{response.class}"
351
- end
352
- response
353
- end
354
-
355
- def handle_cookies(response)
356
- if cookies?(response)
357
- self.cookies = response.get_fields('set-cookie')
358
- logger.info "Cookies set to #{cookies.inspect}"
359
- end
360
- end
361
-
362
- def cookies?(response)
363
- response['set-cookie']
364
- end
365
-
366
- def cookies=(cookies)
367
- @cookies ||= {}
368
-
369
- Array(cookies).each do |cookie|
370
- cookie.match(/(\S+)=([^;]+);?/)
371
-
372
- @cookies[$1] = $2
373
- end
374
-
375
- nil
376
- end
377
-
378
- def cookies
379
- return if @cookies.nil? or @cookies.empty?
380
-
381
- @cookies.map{ |k,v| "#{k}=#{v}" }.join("; ")
382
- end
383
-
384
- def cookie(name)
385
- return if @cookies.nil? or @cookies.empty?
386
-
387
- @cookies[name]
388
- end
389
-
390
- def session=(session)
391
- self.auth_digest = session.auth_digest
392
- self.capabilities = session.capabilities
393
- self.cookies = session.cookies
394
- end
395
-
396
- def session
397
- Session.new(auth_digest, capabilities, cookies)
245
+ res = http_post(capability_url("GetMetadata"),
246
+ { "Format" => "COMPACT",
247
+ "Type" => "METADATA-#{type}",
248
+ "ID" => "0"
249
+ })
250
+ res.body
398
251
  end
399
252
 
400
-
401
253
  # The capabilies as provided by the RETS server during login.
402
254
  #
403
255
  # Currently, only the path in the endpoint URLs is used[1]. Host,
@@ -410,15 +262,21 @@ EOF
410
262
  end
411
263
 
412
264
  def capability_url(name)
413
- url = capabilities[name]
265
+ val = capabilities[name] || capabilities[name.downcase]
266
+
267
+ raise UnknownCapability.new(name) unless val
414
268
 
415
269
  begin
416
- capability_uri = URI.parse(url)
270
+ if val.downcase.match(/^https?:\/\//)
271
+ uri = URI.parse(val)
272
+ else
273
+ uri = URI.parse(login_url)
274
+ uri.path = val
275
+ end
417
276
  rescue URI::InvalidURIError
418
- raise MalformedResponse, "Unable to parse capability URL: #{url.inspect}"
277
+ raise MalformedResponse, "Unable to parse capability URL: #{name} => #{val.inspect}"
419
278
  end
420
-
421
- capability_uri
279
+ uri.to_s
422
280
  end
423
281
 
424
282
  def extract_capabilities(document)
@@ -435,59 +293,16 @@ EOF
435
293
  hash
436
294
  end
437
295
 
438
-
439
-
440
- def connection
441
- @connection ||= options[:persistent] ?
442
- persistent_connection :
443
- Net::HTTP.new(login_uri.host, login_uri.port)
444
- end
445
-
446
- def persistent_connection
447
- conn = Net::HTTP::Persistent.new
448
-
449
- def conn.idempotent?(*)
450
- true
451
- end
452
-
453
- conn
454
- end
455
-
456
-
457
- def user_agent
458
- options[:agent] || "Client/1.0"
296
+ def save_cookie_store(force=nil)
297
+ @http_client.save_cookie_store(force)
459
298
  end
460
299
 
461
- def rets_version
462
- options[:version] || "RETS/1.7.2"
300
+ def http_get(url, params=nil, extra_headers={})
301
+ @http_client.http_get(url, params, extra_headers)
463
302
  end
464
303
 
465
- def build_headers(path)
466
- headers = {
467
- "User-Agent" => user_agent,
468
- "Host" => "#{login_uri.host}:#{login_uri.port}",
469
- "RETS-Version" => rets_version
470
- }
471
-
472
- auth = authorization(path)
473
- headers.merge!("Authorization" => auth) if auth
474
- headers.merge!("Cookie" => cookies) if cookies
475
-
476
- if options[:ua_password]
477
- headers.merge!(
478
- "RETS-UA-Authorization" => build_user_agent_auth(
479
- user_agent, options[:ua_password], '', cookie('RETS-Session-ID'), rets_version))
480
- end
481
-
482
- headers
483
- end
484
-
485
- def build_key_values(data)
486
- data.map{|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
487
- end
488
-
489
- def binary?(data)
490
- data.slice(0, 1024).chars.any? {|b| b >= "\x0" && b < " " && b != '-' && b != '~' && b != "\t" && b != "\r" && b != "\n"}
304
+ def http_post(url, params, extra_headers = {})
305
+ @http_client.http_post(url, params, extra_headers)
491
306
  end
492
307
 
493
308
  def tries
@@ -496,50 +311,46 @@ EOF
496
311
  (@tries += 1) - 1
497
312
  end
498
313
 
499
- class FakeLogger
500
- def fatal(*); end
501
- def error(*); end
502
- def warn(*); end
503
- def info(*); end
504
- def debug(*); end
505
- end
506
-
507
- def format_headers(headers)
508
- out = []
509
-
510
- headers.each do |name, value|
511
- if Array === value
512
- value.each do |v|
513
- out << "#{name}: #{v}"
514
- end
515
- else
516
- out << "#{name}: #{value}"
517
- end
314
+ class FakeLogger < Logger
315
+ def initialize
316
+ super("/dev/null")
518
317
  end
519
-
520
- out.join("\n")
521
318
  end
522
319
 
523
320
  class ErrorChecker
524
321
  def self.check(response)
525
- begin
526
- if !response.body.empty?
322
+ # some RETS servers return success code in XML body but failure code 4xx in http status
323
+ # If xml body is present we ignore http status
324
+
325
+ if !response.body.empty?
326
+ begin
527
327
  xml = Nokogiri::XML.parse(response.body, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
528
328
 
529
329
  rets_element = xml.xpath("/RETS")
330
+ if rets_element.empty?
331
+ return
332
+ end
530
333
  reply_text = (rets_element.attr("ReplyText") || rets_element.attr("replyText")).value
531
334
  reply_code = (rets_element.attr("ReplyCode") || rets_element.attr("replyCode")).value.to_i
532
335
 
533
336
  if reply_code.nonzero?
534
- raise InvalidRequest, "Got error code #{reply_code} (#{reply_text})."
337
+ raise InvalidRequest.new(reply_code, reply_text)
338
+ else
339
+ return
535
340
  end
341
+ rescue Nokogiri::XML::SyntaxError
342
+ #Not xml
536
343
  end
344
+ end
537
345
 
538
- rescue Nokogiri::XML::SyntaxError
539
- #Not xml
346
+ if response.respond_to?(:ok?) && ! response.ok?
347
+ if response.status_code == 401
348
+ raise AuthorizationFailure.new(response.status_code, response.body)
349
+ else
350
+ raise HttpError, "HTTP status: #{response.status_code}, body: #{response.body}"
351
+ end
540
352
  end
541
353
  end
542
354
  end
543
-
544
355
  end
545
356
  end