Class: Rets::Client
Defined Under Namespace
Classes: FakeLogger
Constant Summary
collapse
- COUNT =
Struct.new(:exclude, :include, :only).new(0,1,2)
- DEFAULT_OPTIONS =
{ :persistent => true }
Instance Attribute Summary collapse
Instance Method Summary
collapse
-
#all_objects(opts = {}) ⇒ Object
Returns an array of all objects associated with the given resource.
-
#build_headers ⇒ Object
-
#build_key_values(data) ⇒ Object
-
#capability_url(name) ⇒ Object
-
#connection ⇒ Object
-
#cookies ⇒ Object
-
#cookies=(cookies) ⇒ Object
-
#cookies?(response) ⇒ Boolean
-
#count(opts = {}) ⇒ Object
-
#create_parts_from_response(response) ⇒ Object
-
#decorate_result(result, rets_class) ⇒ Object
-
#decorate_results(results, rets_class) ⇒ Object
-
#extract_capabilities(document) ⇒ Object
-
#extract_digest_header(response) ⇒ Object
-
#fetch_object(object_id, opts = {}) ⇒ Object
-
#find(quantity, opts = {}) ⇒ Object
(also: #search)
-
#find_every(opts = {}) ⇒ Object
-
#find_rets_class(resource_name, rets_class_name) ⇒ Object
-
#fixup_keys(hash) ⇒ Object
Changes keys to be camel cased, per the RETS standard for queries.
-
#format_headers(headers) ⇒ Object
-
#handle_cookies(response) ⇒ Object
-
#handle_response(response) ⇒ Object
-
#handle_unauthorized_response(response) ⇒ Object
-
#initialize(options) ⇒ Client
constructor
A new instance of Client.
-
#login ⇒ Object
Attempts to login by making an empty request to the URL provided in initialize.
-
#metadata_for(type) ⇒ Object
-
#object(object_id, opts = {}) ⇒ Object
-
#objects(object_ids, opts = {}) ⇒ Object
Returns an array of specified objects.
-
#persistent_connection ⇒ Object
-
#raw_request(path, body = nil, headers = build_headers, &reader) ⇒ Object
-
#request(*args, &block) ⇒ Object
-
#request_with_compact_response(path, body, headers) ⇒ Object
-
#retrieve_metadata_type(type) ⇒ Object
-
#rets_version ⇒ Object
-
#session ⇒ Object
-
#session=(session) ⇒ Object
-
#tries ⇒ Object
-
#user_agent ⇒ Object
#build_auth, #build_user_agent_auth, #calculate_digest, #calculate_user_agent_digest
Constructor Details
#initialize(options) ⇒ Client
Returns a new instance of Client.
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# File 'lib/rets/client.rb', line 13
def initialize(options)
@capabilities = nil
@cookies = nil
@metadata = Metadata::Root.new(self)
uri = URI.parse(options[:login_url])
uri.user = options.key?(:username) ? CGI.escape(options[:username]) : nil
uri.password = options.key?(:password) ? CGI.escape(options[:password]) : nil
self.options = DEFAULT_OPTIONS.merge(options)
self.uri = uri
self.logger = options[:logger] || FakeLogger.new
self.session = options[:session] if options[:session]
@cached_metadata = options[:metadata] || nil
end
|
Instance Attribute Details
#authorization ⇒ Object
Returns the value of attribute authorization.
10
11
12
|
# File 'lib/rets/client.rb', line 10
def authorization
@authorization
end
|
#capabilities ⇒ Object
The capabilies as provided by the RETS server during login.
Currently, only the path in the endpoint URLs is used. Host, port, other details remaining constant with those provided to the constructor.
- 1
-
In fact, sometimes only a path is returned from the server.
370
371
372
|
# File 'lib/rets/client.rb', line 370
def capabilities
@capabilities || login
end
|
#logger ⇒ Object
Returns the value of attribute logger.
10
11
12
|
# File 'lib/rets/client.rb', line 10
def logger
@logger
end
|
206
207
208
209
210
211
|
# File 'lib/rets/client.rb', line 206
def metadata
if @cached_metadata && @cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"])
self.metadata = @cached_metadata
end
@metadata
end
|
#options ⇒ Object
Returns the value of attribute options.
10
11
12
|
# File 'lib/rets/client.rb', line 10
def options
@options
end
|
#uri ⇒ Object
Returns the value of attribute uri.
10
11
12
|
# File 'lib/rets/client.rb', line 10
def uri
@uri
end
|
Instance Method Details
#all_objects(opts = {}) ⇒ Object
Returns an array of all objects associated with the given resource.
125
126
127
|
# File 'lib/rets/client.rb', line 125
def all_objects(opts = {})
objects("*", opts)
end
|
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
|
# File 'lib/rets/client.rb', line 427
def
= {
"User-Agent" => user_agent,
"Host" => "#{uri.host}:#{uri.port}",
"RETS-Version" => rets_version
}
.merge!("Authorization" => authorization) if authorization
.merge!("Cookie" => cookies) if cookies
if options[:ua_password]
.merge!(
"RETS-UA-Authorization" => build_user_agent_auth(
user_agent, options[:ua_password], "", rets_version))
end
end
|
#build_key_values(data) ⇒ Object
446
447
448
|
# File 'lib/rets/client.rb', line 446
def build_key_values(data)
data.map{|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
end
|
#capability_url(name) ⇒ Object
374
375
376
377
378
379
380
381
382
383
384
|
# File 'lib/rets/client.rb', line 374
def capability_url(name)
url = capabilities[name]
begin
capability_uri = URI.parse(url)
rescue URI::InvalidURIError => e
raise MalformedResponse, "Unable to parse capability URL: #{url.inspect}"
end
capability_uri
end
|
#connection ⇒ Object
402
403
404
405
406
|
# File 'lib/rets/client.rb', line 402
def connection
@connection ||= options[:persistent] ?
persistent_connection :
Net::HTTP.new(uri.host, uri.port)
end
|
#cookies ⇒ Object
345
346
347
348
349
|
# File 'lib/rets/client.rb', line 345
def cookies
return if @cookies.nil? or @cookies.empty?
@cookies.map{ |k,v| "#{k}=#{v}" }.join("; ")
end
|
#cookies=(cookies) ⇒ Object
333
334
335
336
337
338
339
340
341
342
343
|
# File 'lib/rets/client.rb', line 333
def cookies=(cookies)
@cookies ||= {}
cookies.each do |cookie|
cookie.match(/(\S+)=([^;]+);?/)
@cookies[$1] = $2
end
nil
end
|
#cookies?(response) ⇒ Boolean
329
330
331
|
# File 'lib/rets/client.rb', line 329
def cookies?(response)
response['set-cookie']
end
|
#count(opts = {}) ⇒ Object
68
69
70
71
|
# File 'lib/rets/client.rb', line 68
def count(opts = {})
response = find_every(opts.merge(:count => COUNT.only))
Parser::Compact.parse_count response.body
end
|
#create_parts_from_response(response) ⇒ Object
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
# File 'lib/rets/client.rb', line 140
def create_parts_from_response(response)
content_type = response["content-type"]
if content_type.include?("multipart")
boundary = content_type.scan(/boundary="(.*?)"/).to_s
parts = Parser::Multipart.parse(response.body, boundary)
logger.debug "Found #{parts.size} parts"
return parts
else
= {}
response.each { |k,v| [k] = v }
part = Parser::Multipart::Part.new(, response.body)
return [part]
end
end
|
#decorate_result(result, rets_class) ⇒ Object
117
118
119
120
121
|
# File 'lib/rets/client.rb', line 117
def decorate_result(result, rets_class)
result.each do |key, value|
result[key] = rets_class.find_table(key).resolve(value.to_s)
end
end
|
#decorate_results(results, rets_class) ⇒ Object
111
112
113
114
115
|
# File 'lib/rets/client.rb', line 111
def decorate_results(results, rets_class)
results.map do |result|
decorate_result(result, rets_class)
end
end
|
386
387
388
389
390
391
392
393
394
395
396
397
398
|
# File 'lib/rets/client.rb', line 386
def (document)
raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip
h = Hash.new{|h,k| h.key?(k.downcase) ? h[k.downcase] : nil }
raw_key_values.split(/\n/).
map { |r| r.split(/=/, 2) }.
each { |k,v| h[k.strip.downcase] = v }
h
end
|
274
275
276
277
|
# File 'lib/rets/client.rb', line 274
def (response)
= response.get_fields("www-authenticate")
.detect {|h| h =~ /Digest/}
end
|
#fetch_object(object_id, opts = {}) ⇒ Object
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
|
# File 'lib/rets/client.rb', line 174
def fetch_object(object_id, opts = {})
object_uri = capability_url("GetObject")
body = build_key_values(
"Resource" => opts[:resource],
"Type" => opts[:object_type],
"ID" => "#{opts[:resource_id]}:#{object_id}",
"Location" => 0
)
= .merge(
"Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
"Content-Type" => "application/x-www-form-urlencoded",
"Content-Length" => body.size.to_s
)
request(object_uri.path, body, )
end
|
#find(quantity, opts = {}) ⇒ Object
Also known as:
search
Finds records.
- quantity
-
Return the first record, or an array of records. Uses a symbol :first
or :all
, respectively.
- opts
-
A hash of arguments used to construct the search query, using the following keys:
:search_type
-
Required. The resource to search for.
:class
-
Required. The class of the resource to search for.
:query
-
Required. The DMQL2 query string to execute.
:limit
-
The number of records to request from the server.
:resolve
-
Provide resolved values that use metadata instead of raw system values.
Any other keys are converted to the RETS query format, and passed to the server as part of the query. For instance, the key :offset
will be sent as Offset
.
60
61
62
63
64
65
66
|
# File 'lib/rets/client.rb', line 60
def find(quantity, opts = {})
case quantity
when :first then find_every(opts.merge(:limit => 1)).first
when :all then find_every(opts)
else raise ArgumentError, "First argument must be :first or :all"
end
end
|
#find_every(opts = {}) ⇒ Object
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
# File 'lib/rets/client.rb', line 75
def find_every(opts = {})
search_uri = capability_url("Search")
resolve = opts.delete(:resolve)
= fixup_keys(opts)
defaults = {"QueryType" => "DMQL2", "Format" => "COMPACT"}
query = defaults.merge()
body = build_key_values(query)
= .merge(
"Content-Type" => "application/x-www-form-urlencoded",
"Content-Length" => body.size.to_s
)
results = if opts[:count] == COUNT.only
request(search_uri.path, body, )
else
request_with_compact_response(search_uri.path, body, )
end
if resolve
rets_class = find_rets_class(opts[:search_type], opts[:class])
decorate_results(results, rets_class)
else
results
end
end
|
#find_rets_class(resource_name, rets_class_name) ⇒ Object
107
108
109
|
# File 'lib/rets/client.rb', line 107
def find_rets_class(resource_name, rets_class_name)
metadata.build_tree[resource_name].find_rets_class(rets_class_name)
end
|
#fixup_keys(hash) ⇒ Object
Changes keys to be camel cased, per the RETS standard for queries.
194
195
196
197
198
199
200
201
202
203
204
|
# File 'lib/rets/client.rb', line 194
def fixup_keys(hash)
fixed_hash = {}
hash.each do |key, value|
camel_cased_key = key.to_s.capitalize.gsub(/_(\w)/) { $1.upcase }
fixed_hash[camel_cased_key] = value
end
fixed_hash
end
|
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
|
# File 'lib/rets/client.rb', line 466
def ()
out = []
.each do |name, value|
if Array === value
value.each do |v|
out << "#{name}: #{v}"
end
else
out << "#{name}: #{value}"
end
end
out.join("\n")
end
|
#handle_cookies(response) ⇒ Object
322
323
324
325
326
327
|
# File 'lib/rets/client.rb', line 322
def handle_cookies(response)
if cookies?(response)
self.cookies = response.get_fields('set-cookie')
logger.info "Cookies set to #{cookies.inspect}"
end
end
|
#handle_response(response) ⇒ Object
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
|
# File 'lib/rets/client.rb', line 291
def handle_response(response)
if Net::HTTPUnauthorized === response handle_unauthorized_response(response)
elsif Net::HTTPSuccess === response begin
if !response.body.empty?
xml = Nokogiri::XML.parse(response.body, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
reply_text = xml.xpath("/RETS").attr("ReplyText").value
reply_code = xml.xpath("/RETS").attr("ReplyCode").value.to_i
if reply_code.nonzero?
raise InvalidRequest, "Got error code #{reply_code} (#{reply_text})."
end
end
rescue Nokogiri::XML::SyntaxError => e
logger.debug "Not xml"
end
else
raise UnknownResponse, "Unable to handle response #{response.class}"
end
return response
end
|
#handle_unauthorized_response(response) ⇒ Object
279
280
281
282
283
284
285
286
287
288
289
|
# File 'lib/rets/client.rb', line 279
def handle_unauthorized_response(response)
self.authorization = build_auth((response), uri, tries)
response = raw_request(uri.path)
if Net::HTTPUnauthorized === response
raise AuthorizationFailure, "Authorization failed, check credentials?"
else
self.capabilities = (Nokogiri.parse(response.body))
end
end
|
#login ⇒ Object
36
37
38
39
|
# File 'lib/rets/client.rb', line 36
def login
request(uri.path)
capabilities
end
|
213
214
215
|
# File 'lib/rets/client.rb', line 213
def metadata_for(type)
@metadata.for(type)
end
|
#object(object_id, opts = {}) ⇒ Object
Returns a single object.
resource RETS resource as defined in the resource metadata. object_type an object type defined in the object metadata. resource_id the KeyField value of the given resource instance. object_id can be “*”, or a comma delimited string of one or more integers.
168
169
170
171
172
|
# File 'lib/rets/client.rb', line 168
def object(object_id, opts = {})
response = fetch_object(object_id, opts)
response.body
end
|
#objects(object_ids, opts = {}) ⇒ Object
Returns an array of specified objects.
130
131
132
133
134
135
136
137
138
|
# File 'lib/rets/client.rb', line 130
def objects(object_ids, opts = {})
response = case object_ids
when String then fetch_object(object_ids, opts)
when Array then fetch_object(object_ids.join(","), opts)
else raise ArgumentError, "Expected instance of String or Array, but got #{object_ids.inspect}."
end
create_parts_from_response(response)
end
|
#persistent_connection ⇒ Object
408
409
410
411
412
413
414
415
416
|
# File 'lib/rets/client.rb', line 408
def persistent_connection
conn = Net::HTTP::Persistent.new
def conn.idempotent?(*)
true
end
conn
end
|
#raw_request(path, body = nil, headers = build_headers, &reader) ⇒ Object
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
# File 'lib/rets/client.rb', line 236
def raw_request(path, body = nil, = , &reader)
logger.info "posting to #{path}"
post = Net::HTTP::Post.new(path, )
post.body = body.to_s
logger.debug ""
logger.debug ()
logger.debug body.to_s
connection_args = [Net::HTTP::Persistent === connection ? uri : nil, post].compact
response = connection.request(*connection_args) do |res|
res.read_body(&reader)
end
handle_cookies(response)
logger.debug "Response: (#{response.class})"
logger.debug ""
logger.debug (response.to_hash)
logger.debug ""
logger.debug "Body:"
logger.debug response.body
return response
end
|
#request(*args, &block) ⇒ Object
264
265
266
|
# File 'lib/rets/client.rb', line 264
def request(*args, &block)
handle_response(raw_request(*args, &block))
end
|
#request_with_compact_response(path, body, headers) ⇒ Object
268
269
270
271
272
|
# File 'lib/rets/client.rb', line 268
def request_with_compact_response(path, body, )
response = request(path, body, )
Parser::Compact.parse_document response.body
end
|
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
|
# File 'lib/rets/client.rb', line 217
def retrieve_metadata_type(type)
metadata_uri = capability_url("GetMetadata")
body = build_key_values(
"Format" => "COMPACT",
"Type" => "METADATA-#{type}",
"ID" => "0"
)
= .merge(
"Content-Type" => "application/x-www-form-urlencoded",
"Content-Length" => body.size.to_s
)
response = request(metadata_uri.path, body, )
response.body
end
|
#rets_version ⇒ Object
423
424
425
|
# File 'lib/rets/client.rb', line 423
def rets_version
options[:version] || "RETS/1.7.2"
end
|
#session ⇒ Object
358
359
360
|
# File 'lib/rets/client.rb', line 358
def session
Session.new(authorization, capabilities, cookies)
end
|
#session=(session) ⇒ Object
352
353
354
355
356
|
# File 'lib/rets/client.rb', line 352
def session=(session)
self.authorization = session.authorization
self.capabilities = session.capabilities
self.cookies = session.cookies
end
|
#tries ⇒ Object
452
453
454
455
456
|
# File 'lib/rets/client.rb', line 452
def tries
@tries ||= 1
(@tries += 1) - 1
end
|
#user_agent ⇒ Object
419
420
421
|
# File 'lib/rets/client.rb', line 419
def user_agent
options[:agent] || "Client/1.0"
end
|