rets 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +9 -0
- data/Manifest.txt +0 -1
- data/Rakefile +7 -1
- data/bin/rets +13 -5
- data/lib/rets.rb +32 -11
- data/lib/rets/client.rb +110 -299
- data/lib/rets/metadata/resource.rb +1 -1
- data/lib/rets/metadata/table.rb +1 -0
- data/lib/rets/parser/compact.rb +11 -1
- data/lib/rets/parser/multipart.rb +11 -12
- data/test/fixtures.rb +12 -0
- data/test/helper.rb +4 -4
- data/test/test_client.rb +29 -431
- data/test/test_locking_http_client.rb +29 -0
- data/test/test_metadata.rb +7 -2
- data/test/test_parser_compact.rb +13 -3
- data/test/test_parser_multipart.rb +5 -5
- metadata +17 -11
- data/lib/rets/authentication.rb +0 -57
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
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 << [ "
|
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.
|
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/
|
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/
|
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
|
-
|
6
|
+
class HttpError < StandardError ; end
|
3
7
|
|
4
8
|
class Client
|
5
|
-
DEFAULT_OPTIONS = {
|
9
|
+
DEFAULT_OPTIONS = {}
|
6
10
|
|
7
|
-
include
|
11
|
+
COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
|
8
12
|
|
9
|
-
attr_accessor :
|
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
|
-
|
39
|
-
@
|
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
|
-
|
48
|
-
self.capabilities = extract_capabilities(Nokogiri.parse(
|
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
|
-
|
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
|
-
|
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
|
120
|
-
|
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.
|
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
|
-
|
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 "*"
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
"
|
209
|
-
"
|
210
|
-
|
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"
|
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
|
-
|
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
|
-
|
235
|
+
@client_progress.use_cached_metadata
|
242
236
|
self.metadata = @cached_metadata
|
243
237
|
else
|
244
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
265
|
+
val = capabilities[name] || capabilities[name.downcase]
|
266
|
+
|
267
|
+
raise UnknownCapability.new(name) unless val
|
414
268
|
|
415
269
|
begin
|
416
|
-
|
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: #{
|
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
|
462
|
-
|
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
|
466
|
-
|
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
|
501
|
-
|
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
|
-
|
526
|
-
|
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,
|
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
|
-
|
539
|
-
|
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
|