Class: RETS4R::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/rets4r/client/links.rb,
lib/rets4r/client.rb,
lib/rets4r/client/data.rb,
lib/rets4r/client/metadata.rb,
lib/rets4r/client/requester.rb,
lib/rets4r/client/dataobject.rb,
lib/rets4r/client/exceptions.rb,
lib/rets4r/client/transaction.rb,
lib/rets4r/client/parsers/compact.rb,
lib/rets4r/client/parsers/metadata.rb,
lib/rets4r/client/parsers/response_parser.rb,
lib/rets4r/client/parsers/compact_nokogiri.rb

Overview

:nodoc:

Defined Under Namespace

Classes: AuthRequired, ClientException, CompactDataParser, CompactNokogiriParser, DTDVersionUnavailableException, Data, DataObject, HTTPDebugLogger, HTTPError, InvalidIdentifierException, InvalidQuerySyntaxException, InvalidResourceException, InvalidSelectException, InvalidTypeException, Links, LoginError, MaximumRecordsExceededException, Metadata, MetadataParser, MiscellaneousErrorException, MiscellaneousSearchErrorException, NoObjectFoundException, NoRecordsFoundException, ObjectHeader, ObjectUnavailableException, ParserException, RETSException, RETSTransactionException, RequestTooLargeException, Requester, ResourceUnavailableException, ResponseParser, TimeoutException, TooManyOutstandingQueriesException, TooManyOutstandingRequestsException, Transaction, UnauthorizedQueryException, UnauthorizedRetrievalException, UnknownQueryFieldException, Unsupported, UnsupportedMIMETypeException

Constant Summary collapse

COMPACT_FORMAT =
'COMPACT'
METHOD_GET =
'GET'
METHOD_POST =
'POST'
METHOD_HEAD =
'HEAD'
DEFAULT_METHOD =
METHOD_GET
DEFAULT_RETRY =
2
SUPPORTED_RETS_VERSIONS =
['1.5', '1.7', '1.7.2']
CAPABILITY_LIST =
[
    'Action',
    'ChangePassword',
    'GetObject',
    'Login',
    'LoginComplete',
    'Logout',
    'Search',
    'GetMetadata',
    'Update'
]
RETS_HTTP_MESSAGES =

These are the response messages as defined in the RETS 1.5e2 and 1.7d6 specifications. Provided for convenience and are used by the HTTPError class to provide more useful messages.

{
  '200' => 'Operation successful.',
  '400' => 'The request could not be understood by the server due to malformed syntax.',
  '401' => 'Either the header did not contain an acceptable Authorization or the ' +
                         'username/password was invalid. The server response MUST include a ' +
                         'WWW-Authenticate header field.',
  '402' => 'The requested transaction requires a payment which could not be authorized.',
  '403' => 'The server understood the request, but is refusing to fulfill it.',
  '404' => 'The server has not found anything matching the Request-URI.',
  '405' => 'The method specified in the Request-Line is not allowed for the resource ' +
                         'identified by the Request-URI.',
  '406' => 'The resource identified by the request is only capable of generating response ' +
                         'entities which have content characteristics not acceptable according to the accept ' +
                         'headers sent in the request.',
  '408' => 'The client did not produce a request within the time that the server was prepared to wait.',
  '411' => 'The server refuses to accept the request without a defined Content-Length.',
  '412' => 'Transaction not permitted at this point in the session.',
  '413' => 'The server is refusing to process a request because the request entity is larger than ' +
                         'the server is willing or able to process.',
  '414' => 'The server is refusing to service the request because the Request-URI is longer than ' +
                         'the server is willing to interpret. This error usually only occurs for a GET method.',
  '500' => 'The server encountered an unexpected condition which prevented it from fulfilling ' +
                         'the request.',
  '501' => 'The server does not support the functionality required to fulfill the request.',
  '503' => 'The server is currently unable to handle the request due to a temporary overloading ' +
                         'or maintenance of the server.',
  '505' => 'The server does not support, or refuses to support, the HTTP protocol version that ' +
                         'was used in the request message.',
}
EXCEPTION_TYPES =
{
  # Search Transaction Reply Codes
  20200 => UnknownQueryFieldException,
  20201 => NoRecordsFoundException,
  20202 => InvalidSelectException,
  20203 => MiscellaneousSearchErrorException,
  20206 => InvalidQuerySyntaxException,
  20207 => UnauthorizedQueryException,
  20208 => MaximumRecordsExceededException,
  20209 => TimeoutException,
  20210 => TooManyOutstandingQueriesException,
  20514 => DTDVersionUnavailableException,

  # GetObject Reply Codes
  20400 => InvalidResourceException,
  20401 => InvalidTypeException,
  20402 => InvalidIdentifierException,
  20403 => NoObjectFoundException,
  20406 => UnsupportedMIMETypeException,
  20407 => UnauthorizedRetrievalException,
  20408 => ResourceUnavailableException,
  20409 => ObjectUnavailableException,
  20410 => RequestTooLargeException,
  20411 => TimeoutException,
  20412 => TooManyOutstandingRequestsException,
  20413 => MiscellaneousErrorException

}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url, format = COMPACT_FORMAT) ⇒ Client

Constructor

Requires the URL to the RETS server and takes an optional output format. The output format determines the type of data returned by the various RETS transaction methods.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rets4r/client.rb', line 91

def initialize(url, format = COMPACT_FORMAT)
  @request_struct = RETS4R::Client::Requester.new
  @format   = format
  @urls     = RETS4R::Client::Links.(url)

  @request_method = DEFAULT_METHOD

  @response_parser = RETS4R::Client::ResponseParser.new

  self.mimemap    = {
    'image/jpeg'  => 'jpg',
    'image/gif'   => 'gif'
  }

  if block_given?
    yield self
  end
end

Instance Attribute Details

#formatObject (readonly)

Returns the value of attribute format.



85
86
87
# File 'lib/rets4r/client.rb', line 85

def format
  @format
end

#mimemapObject

Returns the value of attribute mimemap.



84
85
86
# File 'lib/rets4r/client.rb', line 84

def mimemap
  @mimemap
end

#urlsObject (readonly)

Returns the value of attribute urls.



85
86
87
# File 'lib/rets4r/client.rb', line 85

def urls
  @urls
end

Instance Method Details

#count(search_type, klass, query) ⇒ Object



406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/rets4r/client.rb', line 406

def count(search_type, klass, query)
  header = {}
  data = {
    'SearchType' => search_type,
    'Class'      => klass,
    'Query'      => query,
    'QueryType'  => 'DMQL2',
    'Format'     => format,
    'Count'      => '2'
  }
  response = request(@urls.search, data, header)
  result = @response_parser.parse_count(response.body)
  return result
end

#download_metadata(type, id) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/rets4r/client.rb', line 273

def (type, id)
  header = {
    'Accept' => 'text/xml,text/plain;q=0.5'
  }

  data = {
    'Type'   => type,
    'ID'     => id,
    'Format' => @format
  }

  request(@urls., data, header).body
end

#get_header(name) ⇒ Object



144
145
146
# File 'lib/rets4r/client.rb', line 144

def get_header(name)
  @request_struct.headers[name]
end

#get_metadata(type = 'METADATA-SYSTEM', id = '*') ⇒ Object

Requests Metadata from the server. An optional type and id can be specified to request subsets of the Metadata. Please see the RETS specification for more details on this. The format variable tells the server which format to return the Metadata in. Unless you need the raw metadata in a specified format, you really shouldn’t specify the format.

If called with a block, yields the results and returns the value of the block, or returns the metadata directly.



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/rets4r/client.rb', line 261

def (type = 'METADATA-SYSTEM', id = '*')
  xml = (type, id)

  result = @response_parser.(xml, @format)

  if block_given?
    yield result
  else
    result
  end
end

#get_object(resource, type, id, location = false) ⇒ Object

Performs a GetObject transaction on the server. For details on the arguments, please see the RETS specification on GetObject requests.

This method either returns an Array of DataObject instances, or yields each DataObject as it is created. If a block is given, the number of objects yielded is returned.

TODO: how much of this could we move over to WEBrick::HTTPRequest#parse?



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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/rets4r/client.rb', line 294

def get_object(resource, type, id, location = false) #:yields: data_object
  header = {
    'Accept' => mimemap.keys.join(',')
  }

  data = {
    'Resource' => resource,
    'Type'     => type,
    'ID'       => id,
    'Location' => location ? '1' : '0'
  }

  response = request(@urls.objects, data, header)
  results = block_given? ? 0 : []

  if response['content-type'] && response['content-type'].include?('text/xml')
    # This probably means that there was an error.
    # Response parser will likely raise an exception.
    rr = @response_parser.parse_object_response(response.body)
    return rr
  elsif response['content-type'] && response['content-type'].include?('multipart/parallel')
    content_type = process_content_type(response['content-type'])

#        TODO: log this
#        puts "SPLIT ON #{content_type['boundary']}"
    boundary = content_type['boundary']
    if boundary =~ /\s*'([^']*)\s*/
      boundary = $1
    end
    parts = response.body.split("\r\n--#{boundary}")

    parts.shift # Get rid of the initial boundary

#        TODO: log this
#        puts "GOT PARTS #{parts.length}"

    parts.each do |part|
      (raw_header, raw_data) = part.split("\r\n\r\n")

#          TODO: log this
#          puts raw_data.nil?
      next unless raw_data

      data_header = process_header(raw_header)
      data_object = DataObject.new(data_header, raw_data)

      if block_given?
        yield data_object
        results += 1
      else
        results << data_object
      end
    end
  else
    info = {
      'content-type' => response['content-type'], # Compatibility shim.  Deprecated.
      'Content-Type' => response['content-type'],
      'Object-ID'    => response['Object-ID'],
      'Content-ID'   => response['Content-ID']
    }

    if response['Transfer-Encoding'].to_s.downcase == "chunked" || response['Content-Length'].to_i > 100 then
      data_object = DataObject.new(info, response.body)
      if block_given?
        yield data_object
        results += 1
      else
        results << data_object
      end
    end
  end

  results
end

#loggerObject



178
179
180
# File 'lib/rets4r/client.rb', line 178

def logger
  @logger
end

#logger=(logger) ⇒ Object



173
174
175
176
# File 'lib/rets4r/client.rb', line 173

def logger=(logger)
  @logger = logger
  @request_struct.logger = logger
end

#login(username, password) ⇒ Object

Attempts to log into the server using the provided username and password.

If called with a block, the results of the login action are yielded, and logout is called when the block returns. In that case, #login returns the block’s value. If called without a block, returns the result.

As specified in the RETS specification, the Action URL is called and the results made available in the #secondary_results accessor of the results object.



198
199
200
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/rets4r/client.rb', line 198

def (username, password) #:yields: login_results
  @request_struct.username = username
  @request_struct.password = password

  # We are required to set the Accept header to this by the RETS 1.5 specification.
  set_header('Accept', '*/*')

  response = request(@urls.)

  # Parse response to get other URLS
  results = @response_parser.parse_key_value(response.body)

  if (results.success?)
    CAPABILITY_LIST.each do |capability|
      next unless results.response[capability]

      uri = URI.parse(results.response[capability])

      if uri.absolute?
        @urls[capability] = uri
      else
        base = @urls..clone
        base.path = results.response[capability]
        @urls[capability] = base
      end
    end

    logger.debug("Capability URL List: #{@urls.inspect}") if logger
  else
    raise LoginError.new(response.message + "(#{results.reply_code}: #{results.reply_text})")
  end

  # Perform the mandatory get request on the action URL.
  results.secondary_response = perform_action_url

  # We only yield
  if block_given?
    begin
      yield results
    ensure
      self.logout
    end
  else
    results
  end
end

#logoutObject

Logs out of the RETS server.



246
247
248
249
250
251
252
# File 'lib/rets4r/client.rb', line 246

def logout()
  # If no logout URL is provided, then we assume that logout is not necessary (not to
  # mention impossible without a URL). We don't throw an exception, though, but we might
  # want to if this becomes an issue in the future.

  request(@urls.logout) if @urls.logout
end

#request_methodObject



169
170
171
# File 'lib/rets4r/client.rb', line 169

def request_method
  @request_method
end

#request_method=(method) ⇒ Object



164
165
166
167
# File 'lib/rets4r/client.rb', line 164

def request_method=(method)
  @request_method = method
  @request_struct.method = method
end

#rets_versionObject



160
161
162
# File 'lib/rets4r/client.rb', line 160

def rets_version
  @request_struct.rets_version
end

#rets_version=(version) ⇒ Object



156
157
158
# File 'lib/rets4r/client.rb', line 156

def rets_version=(version)
  @request_struct.rets_version = version
end

#search(search_type, klass, query, options = false) ⇒ Object

Peforms a RETS search transaction. Again, please see the RETS specification for details on what these parameters mean. The options parameter takes a hash of options that will added to the search statement.



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/rets4r/client.rb', line 372

def search(search_type, klass, query, options = false)
  header = {}

  # Required Data
  data = {
    'SearchType' => search_type,
    'Class'      => klass,
    'Query'      => query,
    'QueryType'  => 'DMQL2',
    'Format'     => format,
    'Count'      => '0'
  }

  # Options
  #--
  # We might want to switch this to merge!, but I've kept it like this for now because it
  # explicitly casts each value as a string prior to performing the search, so we find out now
  # if can't force a value into the string context. I suppose it doesn't really matter when
  # that happens, though...
  #++
  options.each { |k,v| data[k] = v.to_s } if options

  response = request(@urls.search, data, header)

  # TODO: make parser configurable
  results = RETS4R::Client::CompactNokogiriParser.new(response.body)

  if block_given?
    results.each {|result| yield result}
  else
    return results.to_a
  end
end

#set_header(name, value) ⇒ Object

So very much delegated to the request struct



140
141
142
# File 'lib/rets4r/client.rb', line 140

def set_header(name, value)
  @request_struct.set_header(name, value)
end

#set_pre_request_block(&block) ⇒ Object

Assigns a block that will be called just before the request is sent. This block must accept three parameters:

  • self

  • Net::HTTP instance

  • Hash of headers

The block’s return value will be ignored. If you want to prevent the request to go through, raise an exception.

Example

client = RETS4R::Client.new(...)
# Make a new pre_request_block that calculates the RETS-UA-Authorization header.
client.set_pre_request_block do |rets, http, headers|
  a1 = Digest::MD5.hexdigest([headers["User-Agent"], @password].join(":"))
  if headers.has_key?("Cookie") then
    cookie = headers["Cookie"].split(";").map(&:strip).select {|c| c =~ /rets-session-id/i}
    cookie = cookie ? cookie.split("=").last : ""
  else
    cookie = ""
  end

  parts = [a1, "", cookie, headers["RETS-Version"]]
  headers["RETS-UA-Authorization"] = "Digest " + Digest::MD5.hexdigest(parts.join(":"))
end


135
136
137
# File 'lib/rets4r/client.rb', line 135

def set_pre_request_block(&block)
  @request_struct.pre_request_block = block
end

#user_agentObject



152
153
154
# File 'lib/rets4r/client.rb', line 152

def user_agent
  @request_struct.user_agent
end

#user_agent=(name) ⇒ Object



148
149
150
# File 'lib/rets4r/client.rb', line 148

def user_agent=(name)
  @request_struct.set_header('User-Agent', name)
end