Class: Rets::Client

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

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

Methods included from Authentication

#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

#authorizationObject

Returns the value of attribute authorization.



10
11
12
# File 'lib/rets/client.rb', line 10

def authorization
  @authorization
end

#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.



370
371
372
# File 'lib/rets/client.rb', line 370

def capabilities
  @capabilities || 
end

#loggerObject

Returns the value of attribute logger.



10
11
12
# File 'lib/rets/client.rb', line 10

def logger
  @logger
end

#metadataObject



206
207
208
209
210
211
# File 'lib/rets/client.rb', line 206

def 
  if @cached_metadata && @cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"])
    self. = @cached_metadata
  end
  @metadata
end

#optionsObject

Returns the value of attribute options.



10
11
12
# File 'lib/rets/client.rb', line 10

def options
  @options
end

#uriObject

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

#build_headersObject



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 build_headers
  headers = {
    "User-Agent"   => user_agent,
    "Host"         => "#{uri.host}:#{uri.port}",
    "RETS-Version" => rets_version
  }

  headers.merge!("Authorization" => authorization) if authorization
  headers.merge!("Cookie" => cookies)              if cookies

  if options[:ua_password]
    headers.merge!(
      "RETS-UA-Authorization" => build_user_agent_auth(
        user_agent, options[:ua_password], "", rets_version))
  end

  headers
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

#connectionObject



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

#cookiesObject



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

Returns:

  • (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
    # fake a multipart for interface compatibility
    headers = {}
    response.each { |k,v| headers[k] = v }

    part = Parser::Multipart::Part.new(headers, 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

#extract_capabilities(document) ⇒ Object



386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/rets/client.rb', line 386

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

  h = 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| h[k.strip.downcase] = v }

  h
end

#extract_digest_header(response) ⇒ Object



274
275
276
277
# File 'lib/rets/client.rb', line 274

def extract_digest_header(response)
  authenticate_headers = response.get_fields("www-authenticate")
  authenticate_headers.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
  )

  headers = build_headers.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, 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.



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)

  extras = fixup_keys(opts)

  defaults = {"QueryType" => "DMQL2", "Format" => "COMPACT"}

  query = defaults.merge(extras)

  body = build_key_values(query)

  headers = build_headers.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, headers)
  else
    request_with_compact_response(search_uri.path, body, headers)
  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)
  .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

#format_headers(headers) ⇒ Object



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/rets/client.rb', line 466

def format_headers(headers)
  out = []

  headers.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 # 401
    handle_unauthorized_response(response)

  elsif Net::HTTPSuccess === response # 2xx
    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(extract_digest_header(response), uri, tries)

  response = raw_request(uri.path)

  if Net::HTTPUnauthorized === response
    raise AuthorizationFailure, "Authorization failed, check credentials?"
  else
    self.capabilities = extract_capabilities(Nokogiri.parse(response.body))
  end
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.



36
37
38
39
# File 'lib/rets/client.rb', line 36

def 
  request(uri.path)
  capabilities
end

#metadata_for(type) ⇒ Object



213
214
215
# File 'lib/rets/client.rb', line 213

def (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_connectionObject



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, headers = build_headers, &reader)
  logger.info "posting to #{path}"

  post = Net::HTTP::Post.new(path, headers)
  post.body = body.to_s

  logger.debug ""
  logger.debug format_headers(headers)
  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 format_headers(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, headers)
  response = request(path, body, headers)

  Parser::Compact.parse_document response.body
end

#retrieve_metadata_type(type) ⇒ Object



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 (type)
   = capability_url("GetMetadata")

  body = build_key_values(
    "Format" => "COMPACT",
    "Type"   => "METADATA-#{type}",
    "ID"     => "0"
  )

  headers = build_headers.merge(
    "Content-Type"   => "application/x-www-form-urlencoded",
    "Content-Length" => body.size.to_s
  )

  response = request(.path, body, headers)

  response.body
end

#rets_versionObject



423
424
425
# File 'lib/rets/client.rb', line 423

def rets_version
  options[:version] || "RETS/1.7.2"
end

#sessionObject



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

#triesObject



452
453
454
455
456
# File 'lib/rets/client.rb', line 452

def tries
  @tries ||= 1

  (@tries += 1) - 1
end

#user_agentObject



419
420
421
# File 'lib/rets/client.rb', line 419

def user_agent
  options[:agent] || "Client/1.0"
end