jwulff-rets4r 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,690 @@
1
+ # RETS4R Client
2
+ #
3
+ # Copyright (c) 2006 Scott Patterson <scott.patterson@digitalaun.com>
4
+ #
5
+ # This program is copyrighted free software by Scott Patterson. You can
6
+ # redistribute it and/or modify it under the same terms of Ruby's license;
7
+ # either the dual license version in 2003 (see the file RUBYS), or any later
8
+ # version.
9
+ #
10
+ # TODO
11
+ # 1.0 Support (Adding this support should be fairly easy)
12
+ # 2.0 Support (Adding this support will be very difficult since it is a completely different methodology)
13
+ # Case-insensitive header
14
+
15
+ require 'digest/md5'
16
+ require 'net/http'
17
+ require 'uri'
18
+ require 'cgi'
19
+ require 'auth'
20
+ require 'client/dataobject'
21
+ require 'client/parsers/response_parser'
22
+ require 'thread'
23
+ require 'logger'
24
+
25
+ module RETS4R
26
+ class Client
27
+ COMPACT_FORMAT = 'COMPACT'
28
+
29
+ METHOD_GET = 'GET'
30
+ METHOD_POST = 'POST'
31
+ METHOD_HEAD = 'HEAD'
32
+
33
+ DEFAULT_METHOD = METHOD_GET
34
+ DEFAULT_RETRY = 2
35
+ #DEFAULT_USER_AGENT = 'RETS4R/0.8.2'
36
+ DEFAULT_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5'
37
+ DEFAULT_RETS_VERSION = '1.7'
38
+ SUPPORTED_RETS_VERSIONS = ['1.5', '1.7']
39
+ CAPABILITY_LIST = ['Action', 'ChangePassword', 'GetObject', 'Login', 'LoginComplete', 'Logout', 'Search', 'GetMetadata', 'Update']
40
+
41
+ # These are the response messages as defined in the RETS 1.5e2 and 1.7d6 specifications.
42
+ # Provided for convenience and are used by the HTTPError class to provide more useful
43
+ # messages.
44
+ RETS_HTTP_MESSAGES = {
45
+ '200' => 'Operation successful.',
46
+ '400' => 'The request could not be understood by the server due to malformed syntax.',
47
+ '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.',
48
+ '402' => 'The requested transaction requires a payment which could not be authorized.',
49
+ '403' => 'The server understood the request, but is refusing to fulfill it.',
50
+ '404' => 'The server has not found anything matching the Request-URI.',
51
+ '405' => 'The method specified in the Request-Line is not allowed for the resource identified by the Request-URI.',
52
+ '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.',
53
+ '408' => 'The client did not produce a request within the time that the server was prepared to wait.',
54
+ '411' => 'The server refuses to accept the request without a defined Content-Length.',
55
+ '412' => 'Transaction not permitted at this point in the session.',
56
+ '413' => 'The server is refusing to process a request because the request entity is larger than the server is willing or able to process.',
57
+ '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.',
58
+ '500' => 'The server encountered an unexpected condition which prevented it from fulfilling the request.',
59
+ '501' => 'The server does not support the functionality required to fulfill the request.',
60
+ '503' => 'The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
61
+ '505' => 'The server does not support, or refuses to support, the HTTP protocol version that was used in the request message.',
62
+ }
63
+
64
+ attr_accessor :mimemap, :logger
65
+ attr_reader :format
66
+
67
+ # Constructor
68
+ #
69
+ # Requires the URL to the RETS server and takes an optional output format. The output format
70
+ # determines the type of data returned by the various RETS transaction methods.
71
+ def initialize(url, format = COMPACT_FORMAT)
72
+ @format = format
73
+ @urls = { 'Login' => URI.parse(url) }
74
+ @nc = 0
75
+ @headers = {
76
+ 'User-Agent' => DEFAULT_USER_AGENT,
77
+ 'Accept' => '*/*',
78
+ 'RETS-Version' => "RETS/#{DEFAULT_RETS_VERSION}"#,
79
+ # 'RETS-Session-ID' => '0'
80
+ }
81
+ @request_method = DEFAULT_METHOD
82
+ @semaphore = Mutex.new
83
+
84
+ @response_parser = RETS4R::Client::ResponseParser.new
85
+
86
+ self.mimemap = {
87
+ 'image/jpeg' => 'jpg',
88
+ 'image/gif' => 'gif'
89
+ }
90
+
91
+ if block_given?
92
+ yield self
93
+ end
94
+ end
95
+
96
+ # Assigns a block that will be called just before the request is sent.
97
+ # This block must accept three parameters:
98
+ # * self
99
+ # * Net::HTTP instance
100
+ # * Hash of headers
101
+ #
102
+ # The block's return value will be ignored. If you want to prevent the request
103
+ # to go through, raise an exception.
104
+ #
105
+ # == Example
106
+ #
107
+ # client = RETS4R::Client.new(...)
108
+ # # Make a new pre_request_block that calculates the RETS-UA-Authorization header.
109
+ # client.set_pre_request_block do |rets, http, headers|
110
+ # a1 = Digest::MD5.hexdigest([headers["User-Agent"], @password].join(":"))
111
+ # if headers.has_key?("Cookie") then
112
+ # cookie = headers["Cookie"].split(";").map(&:strip).select {|c| c =~ /rets-session-id/i}
113
+ # cookie = cookie ? cookie.split("=").last : ""
114
+ # else
115
+ # cookie = ""
116
+ # end
117
+ #
118
+ # parts = [a1, "", cookie, headers["RETS-Version"]]
119
+ # headers["RETS-UA-Authorization"] = "Digest " + Digest::MD5.hexdigest(parts.join(":"))
120
+ # end
121
+ def set_pre_request_block(&block)
122
+ @pre_request_block = block
123
+ end
124
+
125
+ # We only allow external read access to URLs because they are internally set based on the
126
+ # results of various queries.
127
+ def urls
128
+ @urls
129
+ end
130
+
131
+ def set_header(name, value)
132
+ if value.nil? then
133
+ @headers.delete(name)
134
+ else
135
+ @headers[name] = value
136
+ end
137
+
138
+ logger.debug("Set header '#{name}' to '#{value}'") if logger
139
+ end
140
+
141
+ def get_header(name)
142
+ @headers[name]
143
+ end
144
+
145
+ def set_user_agent(name)
146
+ set_header('User-Agent', name)
147
+ end
148
+
149
+ def get_user_agent
150
+ get_header('User-Agent')
151
+ end
152
+
153
+ def set_rets_version(version)
154
+ if (SUPPORTED_RETS_VERSIONS.include? version)
155
+ set_header('RETS-Version', "RETS/#{version}")
156
+ else
157
+ raise Unsupported.new("The client does not support RETS version '#{version}'.")
158
+ end
159
+ end
160
+
161
+ def get_rets_version
162
+ (get_header('RETS-Version') || "").gsub("RETS/", "")
163
+ end
164
+
165
+ def set_request_method(method)
166
+ @request_method = method
167
+ end
168
+
169
+ def get_request_method
170
+ # Basic Authentication
171
+ #
172
+ @request_method
173
+ end
174
+
175
+ # Provide more Ruby-like attribute accessors instead of get/set methods
176
+ alias_method :user_agent=, :set_user_agent
177
+ alias_method :user_agent, :get_user_agent
178
+ alias_method :request_method=, :set_request_method
179
+ alias_method :request_method, :get_request_method
180
+ alias_method :rets_version=, :set_rets_version
181
+ alias_method :rets_version, :get_rets_version
182
+
183
+ #### RETS Transaction Methods ####
184
+ #
185
+ # Most of these transaction methods mirror the RETS specification methods, so if you are
186
+ # unsure what they mean, you should check the RETS specification. The latest version can be
187
+ # found at http://www.rets.org
188
+
189
+ # Attempts to log into the server using the provided username and password.
190
+ #
191
+ # If called with a block, the results of the login action are yielded,
192
+ # and logout is called when the block returns. In that case, #login
193
+ # returns the block's value. If called without a block, returns the
194
+ # result.
195
+ #
196
+ # As specified in the RETS specification, the Action URL is called and
197
+ # the results made available in the #secondary_results accessor of the
198
+ # results object.
199
+ def login(username, password) #:yields: login_results
200
+ @username = username
201
+ @password = password
202
+
203
+ # We are required to set the Accept header to this by the RETS 1.5 specification.
204
+ set_header('Accept', '*/*')
205
+
206
+ response = request(@urls['Login'])
207
+
208
+ # Parse response to get other URLS
209
+ results = @response_parser.parse_key_value(response.body)
210
+
211
+ if (results.success?)
212
+ CAPABILITY_LIST.each do |capability|
213
+ next unless results.response[capability]
214
+
215
+ uri = URI.parse(results.response[capability])
216
+
217
+ if uri.absolute?
218
+ @urls[capability] = uri
219
+ else
220
+ base = @urls['Login'].clone
221
+ base.path = results.response[capability]
222
+ @urls[capability] = base
223
+ end
224
+ end
225
+
226
+ logger.debug("Capability URL List: #{@urls.inspect}") if logger
227
+ else
228
+ raise LoginError.new(response.message + "(#{results.reply_code}: #{results.reply_text})")
229
+ end
230
+
231
+ # Perform the mandatory get request on the action URL.
232
+ results.secondary_response = perform_action_url
233
+
234
+ # We only yield
235
+ if block_given?
236
+ begin
237
+ yield results
238
+ ensure
239
+ self.logout
240
+ end
241
+ else
242
+ results
243
+ end
244
+ end
245
+
246
+ # Logs out of the RETS server.
247
+ def logout()
248
+ # If no logout URL is provided, then we assume that logout is not necessary (not to
249
+ # mention impossible without a URL). We don't throw an exception, though, but we might
250
+ # want to if this becomes an issue in the future.
251
+
252
+ request(@urls['Logout']) if @urls['Logout']
253
+ end
254
+
255
+ # Requests Metadata from the server. An optional type and id can be specified to request
256
+ # subsets of the Metadata. Please see the RETS specification for more details on this.
257
+ # The format variable tells the server which format to return the Metadata in. Unless you
258
+ # need the raw metadata in a specified format, you really shouldn't specify the format.
259
+ #
260
+ # If called with a block, yields the results and returns the value of the block, or
261
+ # returns the metadata directly.
262
+ def get_metadata(type = 'METADATA-SYSTEM', id = '*')
263
+ xml = download_metadata(type, id)
264
+
265
+ result = @response_parser.parse_metadata(xml, @format)
266
+
267
+ if block_given?
268
+ yield result
269
+ else
270
+ result
271
+ end
272
+ end
273
+
274
+ def download_metadata(type, id)
275
+ header = {
276
+ 'Accept' => 'text/xml,text/plain;q=0.5'
277
+ }
278
+
279
+ data = {
280
+ 'Type' => type,
281
+ 'ID' => id,
282
+ 'Format' => @format
283
+ }
284
+
285
+ request(@urls['GetMetadata'], data, header).body
286
+ end
287
+
288
+ # Performs a GetObject transaction on the server. For details on the arguments, please see
289
+ # the RETS specification on GetObject requests.
290
+ #
291
+ # This method either returns an Array of DataObject instances, or yields each DataObject
292
+ # as it is created. If a block is given, the number of objects yielded is returned.
293
+ def get_object(resource, type, id, location = 0) #:yields: data_object
294
+ header = {
295
+ 'Accept' => mimemap.keys.join(',')
296
+ }
297
+
298
+ data = {
299
+ 'Resource' => resource,
300
+ 'Type' => type,
301
+ 'ID' => id,
302
+ 'Location' => location.to_s
303
+ }
304
+
305
+ response = request(@urls['GetObject'], data, header)
306
+ results = block_given? ? 0 : []
307
+
308
+ if response['content-type'].include?('text/xml')
309
+ # This probably means that there was an error.
310
+ # Response parser will likely raise an exception.
311
+ rr = @response_parser.parse_object_response(response.body)
312
+ return rr
313
+ elsif response['content-type'].include?('multipart/parallel')
314
+ content_type = process_content_type(response['content-type'])
315
+
316
+ puts "SPLIT ON #{content_type['boundary']}"
317
+ parts = response.body.split("\r\n--#{content_type['boundary']}")
318
+
319
+ parts.shift # Get rid of the initial boundary
320
+
321
+ puts "GOT PARTS #{parts.length}"
322
+
323
+ parts.each do |part|
324
+ (raw_header, raw_data) = part.split("\r\n\r\n")
325
+
326
+ puts raw_data.nil?
327
+ next unless raw_data
328
+
329
+ data_header = process_header(raw_header)
330
+ data_object = DataObject.new(data_header, raw_data)
331
+
332
+ if block_given?
333
+ yield data_object
334
+ results += 1
335
+ else
336
+ results << data_object
337
+ end
338
+ end
339
+ else
340
+ info = {
341
+ 'content-type' => response['content-type'], # Compatibility shim. Deprecated.
342
+ 'Content-Type' => response['content-type'],
343
+ 'Object-ID' => response['Object-ID'],
344
+ 'Content-ID' => response['Content-ID']
345
+ }
346
+
347
+ if response['Transfer-Encoding'].to_s.downcase == "chunked" || response['Content-Length'].to_i > 100 then
348
+ data_object = DataObject.new(info, response.body)
349
+ if block_given?
350
+ yield data_object
351
+ results += 1
352
+ else
353
+ results << data_object
354
+ end
355
+ end
356
+ end
357
+
358
+ results
359
+ end
360
+
361
+ # Peforms a RETS search transaction. Again, please see the RETS specification for details
362
+ # on what these parameters mean. The options parameter takes a hash of options that will
363
+ # added to the search statement.
364
+ def search(search_type, klass, query, options = false)
365
+ header = {}
366
+
367
+ # Required Data
368
+ data = {
369
+ 'SearchType' => search_type,
370
+ 'Class' => klass,
371
+ 'Query' => query,
372
+ 'QueryType' => 'DMQL2',
373
+ 'Format' => format,
374
+ 'Count' => '0'
375
+ }
376
+
377
+ # Options
378
+ #--
379
+ # We might want to switch this to merge!, but I've kept it like this for now because it
380
+ # explicitly casts each value as a string prior to performing the search, so we find out now
381
+ # if can't force a value into the string context. I suppose it doesn't really matter when
382
+ # that happens, though...
383
+ #++
384
+ options.each { |k,v| data[k] = v.to_s } if options
385
+
386
+ response = request(@urls['Search'], data, header)
387
+
388
+ results = @response_parser.parse_results(response.body, @format)
389
+
390
+ if block_given?
391
+ yield results
392
+ else
393
+ return results
394
+ end
395
+ end
396
+
397
+ def count(search_type, klass, query)
398
+ header = {}
399
+ data = {
400
+ 'SearchType' => search_type,
401
+ 'Class' => klass,
402
+ 'Query' => query,
403
+ 'QueryType' => 'DMQL2',
404
+ 'Format' => format,
405
+ 'Count' => '2'
406
+ }
407
+ response = request(@urls['Search'], data, header)
408
+ result = @response_parser.parse_count(response.body)
409
+ return result
410
+ end
411
+
412
+ private
413
+
414
+ # Copied from http.rb
415
+ def basic_encode(account, password)
416
+ 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n")
417
+ end
418
+
419
+ # XXX: This is crap. It does not properly handle quotes.
420
+ def process_content_type(text)
421
+ content = {}
422
+
423
+ field_start = text.index(';')
424
+
425
+ content['content-type'] = text[0 ... field_start].strip
426
+ fields = text[field_start..-1]
427
+
428
+ parts = text.split(';')
429
+
430
+ parts.each do |part|
431
+ (name, value) = part.gsub(/\"/, '').split('=')
432
+
433
+ content[name.strip] = value ? value.strip : value
434
+ end
435
+
436
+ content
437
+ end
438
+
439
+ # Processes the HTTP header
440
+ #--
441
+ # Could we switch over to using CGI for this?
442
+ #++
443
+ def process_header(raw)
444
+ header = {}
445
+
446
+ raw.each do |line|
447
+ (name, value) = line.split(':')
448
+
449
+ header[name.strip] = value.strip if name && value
450
+ end
451
+
452
+ header
453
+ end
454
+
455
+ # Given a hash, it returns a URL encoded query string.
456
+ def create_query_string(hash)
457
+ #parts = hash.map {|key,value| "#{CGI.escape(key)}=#{CGI.escape(value)}"}
458
+ parts = hash.map {|key,value| "#{key}=#{value}"}
459
+ return parts.join('&')
460
+ end
461
+
462
+ # This is the primary transaction method, which the other public methods make use of.
463
+ # Given a url for the transaction (endpoint) it makes a request to the RETS server.
464
+ #
465
+ #--
466
+ # This needs to be better documented, but for now please see the public transaction methods
467
+ # for how to make use of this method.
468
+ #++
469
+ def request(url, data = {}, header = {}, method = @request_method, retry_auth = DEFAULT_RETRY)
470
+ response = ''
471
+
472
+ @semaphore.lock
473
+
474
+ http = Net::HTTP.new(url.host, url.port)
475
+ http.read_timeout = 600
476
+
477
+ if logger && logger.debug?
478
+ http.set_debug_output HTTPDebugLogger.new(logger)
479
+ end
480
+
481
+ http.start do |http|
482
+ begin
483
+ uri = url.path
484
+
485
+ if ! data.empty? && method == METHOD_GET
486
+ uri += "?#{create_query_string(data)}"
487
+ end
488
+
489
+ headers = @headers
490
+ headers.merge(header) unless header.empty?
491
+
492
+ @pre_request_block.call(self, http, headers) if @pre_request_block
493
+
494
+ logger.debug(headers.inspect) if logger
495
+
496
+ @semaphore.unlock
497
+
498
+ response = http.get(uri, headers)
499
+
500
+ @semaphore.lock
501
+
502
+ if response.code == '401'
503
+ # Authentication is required
504
+ raise AuthRequired
505
+ elsif response.code.to_i >= 300
506
+ # We have a non-successful response that we cannot handle
507
+ @semaphore.unlock if @semaphore.locked?
508
+ raise HTTPError.new(response)
509
+ else
510
+ cookies = []
511
+ if set_cookies = response.get_fields('set-cookie') then
512
+ set_cookies.each do |cookie|
513
+ cookies << cookie.split(";").first
514
+ end
515
+ end
516
+ set_header('Cookie', cookies.join("; ")) unless cookies.empty?
517
+ set_header('RETS-Session-ID', response['RETS-Session-ID']) if response['RETS-Session-ID']
518
+ end
519
+ rescue AuthRequired
520
+ @nc += 1
521
+
522
+ if retry_auth > 0
523
+ retry_auth -= 1
524
+ # if response['WWW-Authenticate'].include?('Basic')
525
+ # # Basic Authentication
526
+ # @headers['Authorization'] = basic_encode(@username, @password)
527
+ # else
528
+ # Digest Authentication
529
+ set_header('Authorization', Auth.authenticate(response, @username, @password, url.path, method, @headers['RETS-Request-ID'], get_user_agent, @nc))
530
+ # end
531
+ retry
532
+ else
533
+ @semaphore.unlock if @semaphore.locked?
534
+ raise LoginError.new(response.message)
535
+ end
536
+ end
537
+
538
+ logger.debug(response.body) if logger
539
+ end
540
+
541
+ @semaphore.unlock if @semaphore.locked?
542
+
543
+ return response
544
+ end
545
+
546
+ # If an action URL is present in the URL capability list, it calls that action URL and returns the
547
+ # raw result. Throws a generic RETSException if it is unable to follow the URL.
548
+ def perform_action_url
549
+ begin
550
+ if @urls.has_key?('Action')
551
+ return request(@urls['Action'], {}, {}, METHOD_GET)
552
+ end
553
+ rescue
554
+ raise RETSException.new("Unable to follow action URL: '#{$!}'.")
555
+ end
556
+ end
557
+
558
+ # Provides a proxy class to allow for net/http to log its debug to the logger.
559
+ class HTTPDebugLogger
560
+ def initialize(logger)
561
+ @logger = logger
562
+ end
563
+
564
+ def <<(data)
565
+ @logger.debug(data)
566
+ end
567
+ end
568
+
569
+ #### Exceptions ####
570
+
571
+ # This exception should be thrown when a generic client error is encountered.
572
+ class ClientException < Exception
573
+ end
574
+
575
+ # This exception should be thrown when there is an error with the parser, which is
576
+ # considered a subcomponent of the RETS client. It also includes the XML data that
577
+ # that was being processed at the time of the exception.
578
+ class ParserException < ClientException
579
+ attr_accessor :file
580
+ end
581
+
582
+ # The client does not currently support a specified action.
583
+ class Unsupported < ClientException
584
+ end
585
+
586
+ # The HTTP response returned by the server indicates that there was an error processing
587
+ # the request and the client cannot continue on its own without intervention.
588
+ class HTTPError < ClientException
589
+ attr_accessor :http_response
590
+
591
+ # Takes a HTTPResponse object
592
+ def initialize(http_response)
593
+ self.http_response = http_response
594
+ end
595
+
596
+ # Shorthand for calling HTTPResponse#code
597
+ def code
598
+ http_response.code
599
+ end
600
+
601
+ # Shorthand for calling HTTPResponse#message
602
+ def message
603
+ http_response.message
604
+ end
605
+
606
+ # Returns the RETS specification message for the HTTP response code
607
+ def rets_message
608
+ Client::RETS_HTTP_MESSAGES[code]
609
+ end
610
+
611
+ def to_s
612
+ "#{code} #{message}: #{rets_message}"
613
+ end
614
+ end
615
+
616
+ # A general RETS level exception was encountered. This would include HTTP and RETS
617
+ # specification level errors as well as informative mishaps such as authentication being
618
+ # required for access.
619
+ class RETSException < RuntimeError
620
+ end
621
+
622
+ # There was a problem with logging into the RETS server.
623
+ class LoginError < RETSException
624
+ end
625
+
626
+ # For internal client use only, it is thrown when the a RETS request is made but a password
627
+ # is prompted for.
628
+ class AuthRequired < RETSException
629
+ end
630
+
631
+ # A RETS transaction failed
632
+ class RETSTransactionException < RETSException; end
633
+
634
+ # Search Transaction Exceptions
635
+ class UnknownQueryFieldException < RETSTransactionException; end
636
+ class NoRecordsFoundException < RETSTransactionException; end
637
+ class InvalidSelectException < RETSTransactionException; end
638
+ class MiscellaneousSearchErrorException < RETSTransactionException; end
639
+ class InvalidQuerySyntaxException < RETSTransactionException; end
640
+ class UnauthorizedQueryException < RETSTransactionException; end
641
+ class MaximumRecordsExceededException < RETSTransactionException; end
642
+ class TimeoutException < RETSTransactionException; end
643
+ class TooManyOutstandingQueriesException < RETSTransactionException; end
644
+ class DTDVersionUnavailableException < RETSTransactionException; end
645
+
646
+ # GetObject Exceptions
647
+ class InvalidResourceException < RETSTransactionException; end
648
+ class InvalidTypeException < RETSTransactionException; end
649
+ class InvalidIdentifierException < RETSTransactionException; end
650
+ class NoObjectFoundException < RETSTransactionException; end
651
+ class UnsupportedMIMETypeException < RETSTransactionException; end
652
+ class UnauthorizedRetrievalException < RETSTransactionException; end
653
+ class ResourceUnavailableException < RETSTransactionException; end
654
+ class ObjectUnavailableException < RETSTransactionException; end
655
+ class RequestTooLargeException < RETSTransactionException; end
656
+ class TimeoutException < RETSTransactionException; end
657
+ class TooManyOutstandingRequestsException < RETSTransactionException; end
658
+ class MiscellaneousErrorException < RETSTransactionException; end
659
+
660
+ EXCEPTION_TYPES = {
661
+ # Search Transaction Reply Codes
662
+ 20200 => UnknownQueryFieldException,
663
+ 20201 => NoRecordsFoundException,
664
+ 20202 => InvalidSelectException,
665
+ 20203 => MiscellaneousSearchErrorException,
666
+ 20206 => InvalidQuerySyntaxException,
667
+ 20207 => UnauthorizedQueryException,
668
+ 20208 => MaximumRecordsExceededException,
669
+ 20209 => TimeoutException,
670
+ 20210 => TooManyOutstandingQueriesException,
671
+ 20514 => DTDVersionUnavailableException,
672
+
673
+ # GetObject Reply Codes
674
+ 20400 => InvalidResourceException,
675
+ 20401 => InvalidTypeException,
676
+ 20402 => InvalidIdentifierException,
677
+ 20403 => NoObjectFoundException,
678
+ 20406 => UnsupportedMIMETypeException,
679
+ 20407 => UnauthorizedRetrievalException,
680
+ 20408 => ResourceUnavailableException,
681
+ 20409 => ObjectUnavailableException,
682
+ 20410 => RequestTooLargeException,
683
+ 20411 => TimeoutException,
684
+ 20412 => TooManyOutstandingRequestsException,
685
+ 20413 => MiscellaneousErrorException
686
+
687
+ }
688
+
689
+ end
690
+ end
data/lib/rets4r.rb ADDED
@@ -0,0 +1,5 @@
1
+ # Add lib/rets4r as a default load path.
2
+ dir = File.join File.dirname(__FILE__), 'rets4r'
3
+ $:.unshift(dir) unless $:.include?(dir) || $:.include?(File.expand_path(dir))
4
+
5
+ require 'client'
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Rets4r" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end