rets4r 0.8.5 → 1.1.18
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/{test/client/data/1.5/metadata.xml → .gemtest} +0 -0
- data/CHANGELOG +611 -66
- data/CONTRIBUTORS +6 -2
- data/Gemfile +1 -0
- data/LICENSE +22 -0
- data/MANIFEST +63 -0
- data/NEWS +203 -0
- data/{README → README.rdoc} +11 -4
- data/RUBYS +7 -7
- data/Rakefile +48 -0
- data/TODO +5 -1
- data/examples/client_get_object.rb +31 -42
- data/examples/client_login.rb +20 -18
- data/examples/client_mapper.rb +17 -0
- data/examples/client_metadata.rb +28 -28
- data/examples/client_parser.rb +9 -0
- data/examples/client_search.rb +25 -27
- data/examples/settings.yml +114 -0
- data/lib/rets4r.rb +14 -1
- data/lib/rets4r/auth.rb +70 -66
- data/lib/rets4r/client.rb +470 -650
- data/lib/rets4r/client/data.rb +13 -13
- data/lib/rets4r/client/dataobject.rb +27 -19
- data/lib/rets4r/client/exceptions.rb +116 -0
- data/lib/rets4r/client/links.rb +32 -0
- data/lib/rets4r/client/metadata.rb +12 -12
- data/lib/rets4r/client/parsers/compact.rb +42 -0
- data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
- data/lib/rets4r/client/parsers/metadata.rb +92 -0
- data/lib/rets4r/client/parsers/response_parser.rb +100 -0
- data/lib/rets4r/client/requester.rb +143 -0
- data/lib/rets4r/client/transaction.rb +30 -33
- data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
- data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
- data/lib/rets4r/core_ext/hash/keys.rb +46 -0
- data/lib/rets4r/core_ext/hash/slice.rb +39 -0
- data/lib/rets4r/listing_mapper.rb +17 -0
- data/lib/rets4r/listing_service.rb +35 -0
- data/lib/rets4r/loader.rb +8 -0
- data/lib/tasks/annotations.rake +121 -0
- data/lib/tasks/coverage.rake +13 -0
- data/rets4r.gemspec +24 -0
- data/spec/rets4r_compact_data_parser_spec.rb +7 -0
- data/test/data/1.5/bad_compact.xml +7 -0
- data/test/data/1.5/count_only_compact.xml +3 -0
- data/test/{client/data → data}/1.5/error.xml +0 -0
- data/test/{client/data → data}/1.5/invalid_compact.xml +0 -0
- data/test/{client/data → data}/1.5/login.xml +0 -0
- data/test/data/1.5/metadata.xml +0 -0
- data/test/{client/data → data}/1.5/search_compact.xml +0 -0
- data/test/data/1.5/search_compact_big.xml +136 -0
- data/test/{client/data → data}/1.5/search_unescaped_compact.xml +0 -0
- data/test/data/listing_service.yml +36 -0
- data/test/test_auth.rb +68 -0
- data/test/test_client.rb +342 -0
- data/test/test_client_links.rb +39 -0
- data/test/test_compact_nokogiri.rb +64 -0
- data/test/test_helper.rb +12 -0
- data/test/test_listing_mapper.rb +112 -0
- data/test/test_loader.rb +24 -0
- data/test/test_parser.rb +96 -0
- data/test/test_quality.rb +57 -0
- metadata +168 -53
- data/GPL +0 -340
- data/examples/metadata.xml +0 -42
- data/lib/rets4r/client/metadataindex.rb +0 -82
- data/lib/rets4r/client/parser.rb +0 -141
- data/lib/rets4r/client/parser/rexml.rb +0 -75
- data/lib/rets4r/client/parser/xmlparser.rb +0 -95
- data/test/client/parser/tc_rexml.rb +0 -17
- data/test/client/parser/tc_xmlparser.rb +0 -21
- data/test/client/tc_auth.rb +0 -68
- data/test/client/tc_client.rb +0 -320
- data/test/client/tc_metadataindex.rb +0 -36
- data/test/client/test_parser.rb +0 -128
- data/test/client/ts_all.rb +0 -8
- data/test/ts_all.rb +0 -1
- data/test/ts_client.rb +0 -1
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'rets4r/client/transaction'
|
2
|
+
require 'rets4r/client/parsers/compact'
|
3
|
+
require 'rexml/document'
|
4
|
+
|
5
|
+
module RETS4R
|
6
|
+
class Client
|
7
|
+
class ResponseParser
|
8
|
+
def parse_key_value(xml)
|
9
|
+
parse_common(xml) do |doc|
|
10
|
+
parsed = nil
|
11
|
+
first_child = doc.get_elements('/RETS/RETS-RESPONSE')[0] ? doc.get_elements('/RETS/RETS-RESPONSE')[0] : doc.get_elements('/RETS')[0]
|
12
|
+
unless first_child.nil?
|
13
|
+
parsed = {}
|
14
|
+
first_child.text.each do |line|
|
15
|
+
(key, value) = line.strip.split('=')
|
16
|
+
key.strip! if key
|
17
|
+
value.strip! if value
|
18
|
+
parsed[key] = value
|
19
|
+
end
|
20
|
+
else
|
21
|
+
raise 'Response was not a proper RETS XML doc!'
|
22
|
+
end
|
23
|
+
|
24
|
+
if parsed.nil?
|
25
|
+
raise "Response was not valid key/value format"
|
26
|
+
else
|
27
|
+
parsed
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_results(xml, format)
|
33
|
+
parse_common(xml) do |doc|
|
34
|
+
parser = get_parser_by_name(format)
|
35
|
+
parser.parse_results(doc)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_count(xml)
|
40
|
+
parse_common(xml) do |doc|
|
41
|
+
doc.get_elements('/RETS/COUNT')[0].attributes['Records']
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_metadata(xml, format)
|
46
|
+
parse_common(xml) do |doc|
|
47
|
+
return REXML::Document.new(xml)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_object_response(xml)
|
52
|
+
parse_common(xml) do |doc|
|
53
|
+
# XXX
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def parse_common(xml, &block)
|
60
|
+
if xml == ''
|
61
|
+
raise RETSException, 'No transaction body was returned!'
|
62
|
+
end
|
63
|
+
|
64
|
+
doc = REXML::Document.new(xml)
|
65
|
+
|
66
|
+
root = doc.root
|
67
|
+
if root.nil? || root.name != 'RETS'
|
68
|
+
raise "Response had invalid root node. Document was: #{doc.inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
transaction = Transaction.new
|
72
|
+
transaction.reply_code = root.attributes['ReplyCode']
|
73
|
+
transaction.reply_text = root.attributes['ReplyText']
|
74
|
+
transaction.maxrows = (doc.get_elements('/RETS/MAXROWS').length > 0)
|
75
|
+
|
76
|
+
|
77
|
+
# XXX: If it turns out we need to parse the response of errors, then this will
|
78
|
+
# need to change.
|
79
|
+
if transaction.reply_code.to_i > 0 && transaction.reply_code.to_i != 20201
|
80
|
+
exception_type = Client::EXCEPTION_TYPES[transaction.reply_code.to_i] || RETSTransactionException
|
81
|
+
raise exception_type, "#{transaction.reply_code} - #{transaction.reply_text}"
|
82
|
+
end
|
83
|
+
|
84
|
+
transaction.response = yield doc
|
85
|
+
return transaction
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_parser_by_name(name)
|
89
|
+
case name
|
90
|
+
when 'COMPACT', 'COMPACT-DECODED'
|
91
|
+
type = RETS4R::Client::CompactDataParser
|
92
|
+
else
|
93
|
+
raise "Invalid format #{name}"
|
94
|
+
end
|
95
|
+
type.new
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'rets4r'
|
2
|
+
require 'rets4r/client/exceptions'
|
3
|
+
|
4
|
+
module RETS4R
|
5
|
+
class Client
|
6
|
+
class Requester
|
7
|
+
DEFAULT_USER_AGENT = "rets4r/#{::RETS4R::VERSION}"
|
8
|
+
DEFAULT_RETS_VERSION = '1.7'
|
9
|
+
|
10
|
+
attr_accessor :logger, :headers, :pre_request_block, :nc, :username, :password, :method
|
11
|
+
def initialize
|
12
|
+
@nc = 0
|
13
|
+
@headers = {
|
14
|
+
'User-Agent' => DEFAULT_USER_AGENT,
|
15
|
+
'Accept' => '*/*',
|
16
|
+
'RETS-Version' => "RETS/#{DEFAULT_RETS_VERSION}",
|
17
|
+
}
|
18
|
+
@pre_request_block = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_agent
|
22
|
+
@headers['User-Agent']
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_agent=(name)
|
26
|
+
set_header('User-Agent', name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def rets_version=(version)
|
30
|
+
if (SUPPORTED_RETS_VERSIONS.include? version)
|
31
|
+
set_header('RETS-Version', "RETS/#{version}")
|
32
|
+
else
|
33
|
+
raise Unsupported.new("The client does not support RETS version '#{version}'.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def rets_version
|
38
|
+
(@headers['RETS-Version'] || "").gsub("RETS/", "")
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_header(name, value)
|
42
|
+
if value.nil? then
|
43
|
+
@headers.delete(name)
|
44
|
+
else
|
45
|
+
@headers[name] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
logger.debug("Set header '#{name}' to '#{value}'") if logger
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_agent
|
52
|
+
@headers['User-Agent']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Given a hash, it returns a URL encoded query string.
|
56
|
+
def create_query_string(hash)
|
57
|
+
parts = hash.map {|key,value| "#{CGI.escape(key)}=#{CGI.escape(value)}" unless key.nil? || value.nil?}
|
58
|
+
return parts.join('&')
|
59
|
+
end
|
60
|
+
# This is the primary transaction method, which the other public methods make use of.
|
61
|
+
# Given a url for the transaction (endpoint) it makes a request to the RETS server.
|
62
|
+
#
|
63
|
+
#--
|
64
|
+
# This needs to be better documented, but for now please see the public transaction methods
|
65
|
+
# for how to make use of this method.
|
66
|
+
#++
|
67
|
+
def request(url, data = {}, header = {}, method = @method, retry_auth = DEFAULT_RETRY)
|
68
|
+
response = ''
|
69
|
+
|
70
|
+
http = Net::HTTP.new(url.host, url.port)
|
71
|
+
http.read_timeout = 600
|
72
|
+
|
73
|
+
if logger && logger.debug?
|
74
|
+
http.set_debug_output HTTPDebugLogger.new(logger)
|
75
|
+
end
|
76
|
+
|
77
|
+
http.start do |http|
|
78
|
+
begin
|
79
|
+
uri = url.path
|
80
|
+
|
81
|
+
if ! data.empty? && method == METHOD_GET
|
82
|
+
uri += "?#{create_query_string(data)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
headers = @headers
|
86
|
+
headers.merge(header) unless header.empty?
|
87
|
+
|
88
|
+
@pre_request_block.call(self, http, headers) if @pre_request_block
|
89
|
+
|
90
|
+
logger.debug(headers.inspect) if logger
|
91
|
+
|
92
|
+
post_data = data.map {|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') if method == METHOD_POST
|
93
|
+
response = method == METHOD_POST ? http.post(uri, post_data, headers) :
|
94
|
+
http.get(uri, headers)
|
95
|
+
|
96
|
+
|
97
|
+
if response.code == '401'
|
98
|
+
# Authentication is required
|
99
|
+
raise AuthRequired
|
100
|
+
elsif response.code.to_i >= 300
|
101
|
+
# We have a non-successful response that we cannot handle
|
102
|
+
raise HTTPError.new(response)
|
103
|
+
else
|
104
|
+
cookies = []
|
105
|
+
if set_cookies = response.get_fields('set-cookie') then
|
106
|
+
set_cookies.each do |cookie|
|
107
|
+
cookies << cookie.split(";").first
|
108
|
+
end
|
109
|
+
end
|
110
|
+
set_header('Cookie', cookies.join("; ")) unless cookies.empty?
|
111
|
+
# totally wrong. session id is only ever under the Cookie header
|
112
|
+
#set_header('RETS-Session-ID', response['RETS-Session-ID']) if response['RETS-Session-ID']
|
113
|
+
set_header('RETS-Session-ID',nil)
|
114
|
+
end
|
115
|
+
rescue AuthRequired
|
116
|
+
@nc += 1
|
117
|
+
|
118
|
+
if retry_auth > 0
|
119
|
+
retry_auth -= 1
|
120
|
+
auth = Auth.authenticate(response,
|
121
|
+
@username,
|
122
|
+
@password,
|
123
|
+
url.path,
|
124
|
+
method,
|
125
|
+
@headers['RETS-Request-ID'],
|
126
|
+
@headers['User-Agent'],
|
127
|
+
@nc)
|
128
|
+
set_header('Authorization', auth)
|
129
|
+
retry
|
130
|
+
else
|
131
|
+
raise LoginError.new(response.message)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
logger.debug(response.body) if logger
|
136
|
+
end
|
137
|
+
|
138
|
+
return response
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -1,34 +1,31 @@
|
|
1
1
|
module RETS4R
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
2
|
+
class Client
|
3
|
+
class Transaction
|
4
|
+
attr_accessor :reply_code, :reply_text, :response, :metadata,
|
5
|
+
:header, :maxrows, :delimiter, :secondary_response
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.maxrows = false
|
9
|
+
self.header = []
|
10
|
+
self.delimiter = ?\t
|
11
|
+
end
|
12
|
+
|
13
|
+
def success?
|
14
|
+
return true if self.reply_code == '0'
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
def maxrows?
|
19
|
+
return true if self.maxrows
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
|
23
|
+
def ascii_delimiter
|
24
|
+
self.delimiter.chr
|
25
|
+
end
|
26
|
+
|
27
|
+
# For compatibility with the original library.
|
28
|
+
alias :data :response
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# from ActiveSupport
|
2
|
+
class Array
|
3
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
4
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
5
|
+
#
|
6
|
+
# def options(*args)
|
7
|
+
# args.extract_options!
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# options(1, 2) # => {}
|
11
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
12
|
+
def extract_options!
|
13
|
+
last.is_a?(::Hash) ? pop : {}
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rets4r/core_ext/array/extract_options'
|
2
|
+
# from ActiveSupport
|
3
|
+
|
4
|
+
# Extends the class object with class and instance accessors for class attributes,
|
5
|
+
# just like the native attr* accessors for instance attributes.
|
6
|
+
#
|
7
|
+
# class Person
|
8
|
+
# cattr_accessor :hair_colors
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
12
|
+
class Class
|
13
|
+
def cattr_reader(*syms)
|
14
|
+
syms.flatten.each do |sym|
|
15
|
+
next if sym.is_a?(Hash)
|
16
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
17
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
18
|
+
@@#{sym} = nil # @@hair_colors = nil
|
19
|
+
end # end
|
20
|
+
#
|
21
|
+
def self.#{sym} # def self.hair_colors
|
22
|
+
@@#{sym} # @@hair_colors
|
23
|
+
end # end
|
24
|
+
#
|
25
|
+
def #{sym} # def hair_colors
|
26
|
+
@@#{sym} # @@hair_colors
|
27
|
+
end # end
|
28
|
+
EOS
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def cattr_writer(*syms)
|
33
|
+
options = syms.extract_options!
|
34
|
+
syms.flatten.each do |sym|
|
35
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
36
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
37
|
+
@@#{sym} = nil # @@hair_colors = nil
|
38
|
+
end # end
|
39
|
+
#
|
40
|
+
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
41
|
+
@@#{sym} = obj # @@hair_colors = obj
|
42
|
+
end # end
|
43
|
+
#
|
44
|
+
#{" #
|
45
|
+
def #{sym}=(obj) # def hair_colors=(obj)
|
46
|
+
@@#{sym} = obj # @@hair_colors = obj
|
47
|
+
end # end
|
48
|
+
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
49
|
+
EOS
|
50
|
+
self.send("#{sym}=", yield) if block_given?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def cattr_accessor(*syms, &blk)
|
55
|
+
cattr_reader(*syms)
|
56
|
+
cattr_writer(*syms, &blk)
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# from ActiveSupport
|
2
|
+
class Hash
|
3
|
+
# Return a new hash with all keys converted to strings.
|
4
|
+
def stringify_keys
|
5
|
+
dup.stringify_keys!
|
6
|
+
end
|
7
|
+
|
8
|
+
# Destructively convert all keys to strings.
|
9
|
+
def stringify_keys!
|
10
|
+
keys.each do |key|
|
11
|
+
self[key.to_s] = delete(key)
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return a new hash with all keys converted to symbols, as long as
|
17
|
+
# they respond to +to_sym+.
|
18
|
+
def symbolize_keys
|
19
|
+
dup.symbolize_keys!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Destructively convert all keys to symbols, as long as they respond
|
23
|
+
# to +to_sym+.
|
24
|
+
def symbolize_keys!
|
25
|
+
keys.each do |key|
|
26
|
+
self[(key.to_sym rescue key) || key] = delete(key)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :to_options, :symbolize_keys
|
32
|
+
alias_method :to_options!, :symbolize_keys!
|
33
|
+
|
34
|
+
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
35
|
+
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
36
|
+
# as keys, this will fail.
|
37
|
+
#
|
38
|
+
# ==== Examples
|
39
|
+
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
|
40
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
|
41
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
42
|
+
def assert_valid_keys(*valid_keys)
|
43
|
+
unknown_keys = keys - [valid_keys].flatten
|
44
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# from ActiveSupport
|
2
|
+
class Hash
|
3
|
+
# Slice a hash to include only the given keys. This is useful for
|
4
|
+
# limiting an options hash to valid keys before passing to a method:
|
5
|
+
#
|
6
|
+
# def search(criteria = {})
|
7
|
+
# assert_valid_keys(:mass, :velocity, :time)
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# search(options.slice(:mass, :velocity, :time))
|
11
|
+
#
|
12
|
+
# If you have an array of keys you want to limit to, you should splat them:
|
13
|
+
#
|
14
|
+
# valid_keys = [:mass, :velocity, :time]
|
15
|
+
# search(options.slice(*valid_keys))
|
16
|
+
def slice(*keys)
|
17
|
+
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
18
|
+
hash = self.class.new
|
19
|
+
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
20
|
+
hash
|
21
|
+
end
|
22
|
+
|
23
|
+
# Replaces the hash with only the given keys.
|
24
|
+
# Returns a hash contained the removed key/value pairs
|
25
|
+
# {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4}
|
26
|
+
def slice!(*keys)
|
27
|
+
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
28
|
+
omit = slice(*self.keys - keys)
|
29
|
+
hash = slice(*keys)
|
30
|
+
replace(hash)
|
31
|
+
omit
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract!(*keys)
|
35
|
+
result = {}
|
36
|
+
keys.each {|key| result[key] = delete(key) }
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|