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.
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/VERSION.yml +4 -0
- data/lib/rets4r/auth.rb +73 -0
- data/lib/rets4r/client/data.rb +14 -0
- data/lib/rets4r/client/dataobject.rb +20 -0
- data/lib/rets4r/client/metadata.rb +15 -0
- data/lib/rets4r/client/metadataindex.rb +82 -0
- data/lib/rets4r/client/parser.rb +135 -0
- data/lib/rets4r/client/parsers/compact.rb +41 -0
- data/lib/rets4r/client/parsers/metadata.rb +92 -0
- data/lib/rets4r/client/parsers/response_parser.rb +103 -0
- data/lib/rets4r/client/transaction.rb +34 -0
- data/lib/rets4r/client.rb +690 -0
- data/lib/rets4r.rb +5 -0
- data/spec/rets4r_spec.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- metadata +74 -0
@@ -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