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/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/metadata_request.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, MetadataRequest, 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

}
MetadataParser =

Kept for compatibility with previous versions.

::CompactDocument

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.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rets4r/client.rb', line 58

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.



52
53
54
# File 'lib/rets4r/client.rb', line 52

def format
  @format
end

#mimemapObject

Returns the value of attribute mimemap.



51
52
53
# File 'lib/rets4r/client.rb', line 51

def mimemap
  @mimemap
end

#urlsObject (readonly)

Returns the value of attribute urls.



52
53
54
# File 'lib/rets4r/client.rb', line 52

def urls
  @urls
end

Instance Method Details

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



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/rets4r/client.rb', line 380

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

  # Required Data
  data = {
    'SearchType' => search_type,
    'Class'      => klass,
    'Query'      => query,
    'QueryType'  => 'DMQL2',
    'Format'     => format,
    'Count'      => '2'
  }
  options.each { |k,v| data[k] = v.to_s } if options
  response = request(@urls.search, data, header)
  # TODO: fix test to like this
  # ResponseDocument.safe_parse(xml).validate!.parse_count
  @response_parser.parse_count(response.body)
end

#download_metadata(type, id) ⇒ Object



255
256
257
258
# File 'lib/rets4r/client.rb', line 255

def (type, id)
  req = MetadataRequest.new(@urls., type, id, @format, @request_struct)
  req.request.body
end

#get_header(name) ⇒ Object



122
123
124
# File 'lib/rets4r/client.rb', line 122

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.



241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/rets4r/client.rb', line 241

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

  result = @response_parser.(xml, @format)
  # TODO: fix test to like this
  # result = ResponseDocument.safe_parse(xml).validate!.to_rexml

  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?



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/rets4r/client.rb', line 267

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.
    # TODO: test this
    rr = ResponseDocument.safe_parse(response.body).validate!.to_transaction
    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



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

def logger
  @logger
end

#logger=(logger) ⇒ Object



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

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.



176
177
178
179
180
181
182
183
184
185
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
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/rets4r/client.rb', line 176

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)
  # TODO: fix test to like this
  # results = ResponseDocument.safe_parse(response.body).validate!.parse_key_value

  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.



226
227
228
229
230
231
232
# File 'lib/rets4r/client.rb', line 226

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



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

def request_method
  @request_method
end

#request_method=(method) ⇒ Object



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

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

#rets_versionObject



138
139
140
# File 'lib/rets4r/client.rb', line 138

def rets_version
  @request_struct.rets_version
end

#rets_version=(version) ⇒ Object



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

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.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/rets4r/client.rb', line 346

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



118
119
120
# File 'lib/rets4r/client.rb', line 118

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

#set_post_request_block(&block) ⇒ Object

Assigns a block that will be called just before the response is returned to the calling method. This block must accept three parameters:

  • self

  • Net::HTTP instance

  • Hash of headers

The block’s return value will be ignored.



113
114
115
# File 'lib/rets4r/client.rb', line 113

def set_post_request_block(&block)
  @request_struct.post_request_block = block
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


102
103
104
# File 'lib/rets4r/client.rb', line 102

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

#user_agentObject



130
131
132
# File 'lib/rets4r/client.rb', line 130

def user_agent
  @request_struct.user_agent
end

#user_agent=(name) ⇒ Object



126
127
128
# File 'lib/rets4r/client.rb', line 126

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