Class: Rets::Client

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

Defined Under Namespace

Classes: ErrorChecker, 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.



288
289
290
# File 'lib/rets/client.rb', line 288

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



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

def 
  return @metadata if @metadata

  if @cached_metadata && (@options[:skip_metadata_uptodate_check] ||
      @cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
    @client_progress.
    self. = @cached_metadata
  else
    @client_progress.(@cached_metadata)
    self. = Metadata::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.



171
172
173
# File 'lib/rets/client.rb', line 171

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

#capability_url(name) ⇒ Object

Raises:



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/rets/client.rb', line 292

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

  raise UnknownCapability.new(name) 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]

  @cached_metadata   = nil
  @capabilities      = nil
  @metadata          = nil
  @tries             = nil
  self.capabilities  = nil

  self.logger      = @options[:logger] || FakeLogger.new
  @client_progress = ClientProgressReporter.new(self.logger, options[:stats_collector], options[:stats_prefix])
  @cached_metadata = @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



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/rets/client.rb', line 186

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



157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/rets/client.rb', line 157

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
      raise "Value could not be interpreted. Key #{key} Value #{value}"
      @client_progress.(key)
    end
  end
end

#decorate_results(results, rets_class) ⇒ Object



151
152
153
154
155
# File 'lib/rets/client.rb', line 151

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

#extract_capabilities(document) ⇒ Object



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

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



223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/rets/client.rb', line 223

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.



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

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



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

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", "binary", :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



147
148
149
# File 'lib/rets/client.rb', line 147

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

#find_with_retries(opts = {}) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/rets/client.rb', line 112

def find_with_retries(opts = {})
  retries = 0
  resolve = opts.delete(:resolve)
  begin
    find_every(opts, resolve)
  rescue AuthorizationFailure, InvalidRequest => e
    if retries < opts.fetch(:max_retries, 3)
      retries += 1
      @client_progress.find_with_retries_failed_a_retry(e, retries)
      clean_setup
      retry
    else
      @client_progress.find_with_retries_exceeded_retry_count(e)
      raise e
    end
  end
end

#fixup_keys(hash) ⇒ Object

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



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

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

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



328
329
330
# File 'lib/rets/client.rb', line 328

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

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



332
333
334
# File 'lib/rets/client.rb', line 332

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 retsdoc.onconfluence.com/display/rets172/4.10+Capability+URL+List.

Raises:



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

def 
  res = http_get()
  unless res.status_code == 200
    raise UnknownResponse, "bad response to login, expected a 200, but got #{res.status_code}. Body was #{res.body}."
  end
  self.capabilities = extract_capabilities(Nokogiri.parse(res.body))
  raise UnknownResponse, "Cannot read rets server capabilities." unless @capabilities
  @capabilities
end

#logoutObject



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

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.



218
219
220
221
# File 'lib/rets/client.rb', line 218

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.



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

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



264
265
266
267
268
269
270
# File 'lib/rets/client.rb', line 264

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

#retrieve_metadata_type(type) ⇒ Object



272
273
274
275
276
277
278
279
# File 'lib/rets/client.rb', line 272

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


324
325
326
# File 'lib/rets/client.rb', line 324

def save_cookie_store(force=nil)
  @http_client.save_cookie_store(force)
end

#triesObject



336
337
338
339
340
# File 'lib/rets/client.rb', line 336

def tries
  @tries ||= 1

  (@tries += 1) - 1
end