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 +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
|