Class: Rets::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/rets/client.rb

Defined Under Namespace

Classes: FakeLogger

Constant Summary collapse

DEFAULT_OPTIONS =
{}
COUNT =
Struct.new(:exclude, :include, :only).new(0,1,2)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Client

Returns a new instance of Client.



16
17
18
19
# File 'lib/rets/client.rb', line 16

def initialize(options)
  @options = options
  clean_setup
end

Instance Attribute Details

#capabilitiesObject

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.



303
304
305
# File 'lib/rets/client.rb', line 303

def capabilities
  @capabilities || 
end

#loggerObject

Returns the value of attribute logger.



13
14
15
# File 'lib/rets/client.rb', line 13

def logger
  @logger
end

#login_urlObject

Returns the value of attribute login_url.



13
14
15
# File 'lib/rets/client.rb', line 13

def 
  @login_url
end

#metadataObject



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/rets/client.rb', line 266

def 
  return  if 

  if  && (@options[:skip_metadata_uptodate_check] ||
      .current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
    @client_progress.
    self. = 
  else
    @client_progress.()
    self. = ::Root.new(logger, )
  end
end

#optionsObject

Returns the value of attribute options.



13
14
15
# File 'lib/rets/client.rb', line 13

def options
  @options
end

Instance Method Details

#all_objects(opts = {}) ⇒ Object

Returns an array of all objects associated with the given resource.



186
187
188
# File 'lib/rets/client.rb', line 186

def all_objects(opts = {})
  objects("*", opts)
end

#capability_url(name) ⇒ Object

Raises:



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/rets/client.rb', line 307

def capability_url(name)
  val = capabilities[name] || capabilities[name.downcase]

  raise UnknownCapability.new(name, capabilities.keys) unless val

  begin
    if val.downcase.match(/^https?:\/\//)
      uri = URI.parse(val)
    else
      uri = URI.parse()
      uri.path = val
    end
  rescue URI::InvalidURIError
    raise MalformedResponse, "Unable to parse capability URL: #{name} => #{val.inspect}"
  end
  uri.to_s
end

#clean_setupObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/rets/client.rb', line 21

def clean_setup
  self.options     = DEFAULT_OPTIONS.merge(@options)
  self.   = self.options[:login_url]

     = nil
  @capabilities      = nil
            = nil
  @tries             = nil
  self.capabilities  = nil

  self.logger      = @options[:logger] || FakeLogger.new
  @client_progress = ClientProgressReporter.new(self.logger, options[:stats_collector], options[:stats_prefix])
   = @options[:metadata]
  if @options[:http_proxy]
    @http = HTTPClient.new(options.fetch(:http_proxy))

    if @options[:proxy_username]
      @http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
    end
  else
    @http = HTTPClient.new
  end

  if @options[:receive_timeout]
    @http.receive_timeout = @options[:receive_timeout]
  end

  @http.set_cookie_store(options[:cookie_store]) if options[:cookie_store]

  @http_client = Rets::HttpClient.new(@http, @options, @logger, @login_url)
  if options[:http_timing_stats_collector]
    @http_client = Rets::MeasuringHttpClient.new(@http_client, options.fetch(:http_timing_stats_collector), options.fetch(:http_timing_stats_prefix))
  end
  if options[:lock_around_http_requests]
    @http_client = Rets::LockingHttpClient.new(@http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
  end
end

#create_parts_from_response(response) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/rets/client.rb', line 201

def create_parts_from_response(response)
  content_type = response.header["content-type"][0]

  if content_type.nil?
    raise MalformedResponse, "Unable to read content-type from response: #{response.inspect}"
  end

  if content_type.include?("multipart")
    boundary = content_type.scan(/boundary="?([^;"]*)?/).join

    parts = Parser::Multipart.parse(response.body, boundary)

    logger.debug "Rets::Client: Found #{parts.size} parts"

    return parts
  else
    # fake a multipart for interface compatibility
    headers = {}
    response.headers.each { |k,v| headers[k] = v[0] }

    part = Parser::Multipart::Part.new(headers, response.body)

    return [part]
  end
end

#decorate_result(result, rets_class) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/rets/client.rb', line 173

def decorate_result(result, rets_class)
  result.each do |key, value|
    table = rets_class.find_table(key)
    if table
      result[key] = table.resolve(value.to_s)
    else
      #can't resolve just leave the value be
      @client_progress.(key)
    end
  end
end

#decorate_results(results, rets_class) ⇒ Object



167
168
169
170
171
# File 'lib/rets/client.rb', line 167

def decorate_results(results, rets_class)
  results.map do |result|
    decorate_result(result, rets_class)
  end
end

#extract_capabilities(document) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/rets/client.rb', line 325

def extract_capabilities(document)
  raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip

  hash = Hash.new{|h,k| h.key?(k.downcase) ? h[k.downcase] : nil }

  # ... :(
  # Feel free to make this better. It has a test.
  raw_key_values.split(/\n/).
    map  { |r| r.split(/\=/, 2) }.
    each { |k,v| hash[k.strip.downcase] = v.strip }

  hash
end

#fetch_object(object_id, opts = {}) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/rets/client.rb', line 238

def fetch_object(object_id, opts = {})
  params = {
    "Resource" => opts.fetch(:resource),
    "Type"     => opts.fetch(:object_type),
    "ID"       => "#{opts.fetch(:resource_id)}:#{object_id}",
    "Location" => opts.fetch(:location, 0)
  }

  extra_headers = {
    "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
  }

  http_post(capability_url("GetObject"), params, extra_headers)
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.



101
102
103
104
105
106
107
# File 'lib/rets/client.rb', line 101

def find(quantity, opts = {})
  case quantity
    when :first  then find_with_retries(opts.merge(:limit => 1)).first
    when :all    then find_with_retries(opts)
    else raise ArgumentError, "First argument must be :first or :all"
  end
end

#find_every(opts, resolve) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/rets/client.rb', line 144

def find_every(opts, resolve)
  params = {"QueryType" => "DMQL2", "Format" => "COMPACT"}.merge(fixup_keys(opts))
  res = http_post(capability_url("Search"), params)

  if opts[:count] == COUNT.only
    Parser::Compact.get_count(res.body)
  else
    results = Parser::Compact.parse_document(
      res.body.encode("UTF-8", res.body.encoding, :invalid => :replace, :undef => :replace)
    )
    if resolve
      rets_class = find_rets_class(opts[:search_type], opts[:class])
      decorate_results(results, rets_class)
    else
      results
    end
  end
end

#find_rets_class(resource_name, rets_class_name) ⇒ Object



163
164
165
# File 'lib/rets/client.rb', line 163

def find_rets_class(resource_name, rets_class_name)
  .tree[resource_name].find_rets_class(rets_class_name)
end

#find_with_given_retry(retries, resolve, opts) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/rets/client.rb', line 117

def find_with_given_retry(retries, resolve, opts)
  begin
    find_every(opts, resolve)
  rescue NoRecordsFound => e
    if opts.fetch(:no_records_not_an_error, false)
      @client_progress.no_records_found
      opts[:count] == COUNT.only ? 0 : []
    else
      handle_find_failure(retries, resolve, opts, e)
    end
  rescue AuthorizationFailure, InvalidRequest => e
    handle_find_failure(retries, resolve, opts, e)
  end
end

#find_with_retries(opts = {}) ⇒ Object



111
112
113
114
115
# File 'lib/rets/client.rb', line 111

def find_with_retries(opts = {})
  retries = 0
  resolve = opts.delete(:resolve)
  find_with_given_retry(retries, resolve, opts)
end

#fixup_keys(hash) ⇒ Object

Changes keys to be camel cased, per the RETS standard for queries.



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/rets/client.rb', line 254

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

#handle_find_failure(retries, resolve, opts, e) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/rets/client.rb', line 132

def handle_find_failure(retries, resolve, opts, e)
  if retries < opts.fetch(:max_retries, 3)
    retries += 1
    @client_progress.find_with_retries_failed_a_retry(e, retries)
    clean_setup
    find_with_given_retry(retries, resolve, opts)
  else
    @client_progress.find_with_retries_exceeded_retry_count(e)
    raise e
  end
end

#http_get(url, params = nil, extra_headers = {}) ⇒ Object



343
344
345
# File 'lib/rets/client.rb', line 343

def http_get(url, params=nil, extra_headers={})
  @http_client.http_get(url, params, extra_headers)
end

#http_post(url, params, extra_headers = {}) ⇒ Object



347
348
349
# File 'lib/rets/client.rb', line 347

def http_post(url, params, extra_headers = {})
  @http_client.http_post(url, params, extra_headers)
end

#loginObject

Attempts to login by making an empty request to the URL provided in initialize. Returns the capabilities that the RETS server provides, per page 34 of www.realtor.org/retsorg.nsf/retsproto1.7d6.pdf#page=34

Raises:



62
63
64
65
66
67
68
69
# File 'lib/rets/client.rb', line 62

def 
  res = http_get()
  Parser::ErrorChecker.check(res)

  self.capabilities = extract_capabilities(Nokogiri.parse(res.body))
  raise UnknownResponse, "Cannot read rets server capabilities." unless @capabilities
  @capabilities
end

#logoutObject



71
72
73
74
75
76
77
78
79
80
# File 'lib/rets/client.rb', line 71

def logout
  unless capabilities["Logout"]
    raise NoLogout.new('No logout method found for rets client')
  end
  http_get(capability_url("Logout"))
rescue UnknownResponse => e
  unless e.message.match(/expected a 200, but got 401/)
    raise e
  end
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 colon delimited string of integers or an array of integers.



233
234
235
236
# File 'lib/rets/client.rb', line 233

def object(object_id, opts = {})
  response = fetch_object(Array(object_id).join(':'), opts)
  response.body
end

#objects(object_ids, opts = {}) ⇒ Object

Returns an array of specified objects.



191
192
193
194
195
196
197
198
199
# File 'lib/rets/client.rb', line 191

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

#retrieve_metadataObject



279
280
281
282
283
284
285
# File 'lib/rets/client.rb', line 279

def 
   = {}
  ::.each {|type|
    [type] = (type)
  }
  
end

#retrieve_metadata_type(type) ⇒ Object



287
288
289
290
291
292
293
294
# File 'lib/rets/client.rb', line 287

def (type)
  res = http_post(capability_url("GetMetadata"),
                  { "Format" => "COMPACT",
                    "Type"   => "METADATA-#{type}",
                    "ID"     => "0"
                  })
  res.body
end


339
340
341
# File 'lib/rets/client.rb', line 339

def save_cookie_store
  @http_client.save_cookie_store
end

#triesObject



351
352
353
354
355
# File 'lib/rets/client.rb', line 351

def tries
  @tries ||= 1

  (@tries += 1) - 1
end