rets4r 0.8.5 → 1.1.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/.document +5 -0
  2. data/{test/client/data/1.5/metadata.xml → .gemtest} +0 -0
  3. data/CHANGELOG +611 -66
  4. data/CONTRIBUTORS +6 -2
  5. data/Gemfile +1 -0
  6. data/LICENSE +22 -0
  7. data/MANIFEST +63 -0
  8. data/NEWS +203 -0
  9. data/{README → README.rdoc} +11 -4
  10. data/RUBYS +7 -7
  11. data/Rakefile +48 -0
  12. data/TODO +5 -1
  13. data/examples/client_get_object.rb +31 -42
  14. data/examples/client_login.rb +20 -18
  15. data/examples/client_mapper.rb +17 -0
  16. data/examples/client_metadata.rb +28 -28
  17. data/examples/client_parser.rb +9 -0
  18. data/examples/client_search.rb +25 -27
  19. data/examples/settings.yml +114 -0
  20. data/lib/rets4r.rb +14 -1
  21. data/lib/rets4r/auth.rb +70 -66
  22. data/lib/rets4r/client.rb +470 -650
  23. data/lib/rets4r/client/data.rb +13 -13
  24. data/lib/rets4r/client/dataobject.rb +27 -19
  25. data/lib/rets4r/client/exceptions.rb +116 -0
  26. data/lib/rets4r/client/links.rb +32 -0
  27. data/lib/rets4r/client/metadata.rb +12 -12
  28. data/lib/rets4r/client/parsers/compact.rb +42 -0
  29. data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
  30. data/lib/rets4r/client/parsers/metadata.rb +92 -0
  31. data/lib/rets4r/client/parsers/response_parser.rb +100 -0
  32. data/lib/rets4r/client/requester.rb +143 -0
  33. data/lib/rets4r/client/transaction.rb +30 -33
  34. data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
  35. data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
  36. data/lib/rets4r/core_ext/hash/keys.rb +46 -0
  37. data/lib/rets4r/core_ext/hash/slice.rb +39 -0
  38. data/lib/rets4r/listing_mapper.rb +17 -0
  39. data/lib/rets4r/listing_service.rb +35 -0
  40. data/lib/rets4r/loader.rb +8 -0
  41. data/lib/tasks/annotations.rake +121 -0
  42. data/lib/tasks/coverage.rake +13 -0
  43. data/rets4r.gemspec +24 -0
  44. data/spec/rets4r_compact_data_parser_spec.rb +7 -0
  45. data/test/data/1.5/bad_compact.xml +7 -0
  46. data/test/data/1.5/count_only_compact.xml +3 -0
  47. data/test/{client/data → data}/1.5/error.xml +0 -0
  48. data/test/{client/data → data}/1.5/invalid_compact.xml +0 -0
  49. data/test/{client/data → data}/1.5/login.xml +0 -0
  50. data/test/data/1.5/metadata.xml +0 -0
  51. data/test/{client/data → data}/1.5/search_compact.xml +0 -0
  52. data/test/data/1.5/search_compact_big.xml +136 -0
  53. data/test/{client/data → data}/1.5/search_unescaped_compact.xml +0 -0
  54. data/test/data/listing_service.yml +36 -0
  55. data/test/test_auth.rb +68 -0
  56. data/test/test_client.rb +342 -0
  57. data/test/test_client_links.rb +39 -0
  58. data/test/test_compact_nokogiri.rb +64 -0
  59. data/test/test_helper.rb +12 -0
  60. data/test/test_listing_mapper.rb +112 -0
  61. data/test/test_loader.rb +24 -0
  62. data/test/test_parser.rb +96 -0
  63. data/test/test_quality.rb +57 -0
  64. metadata +168 -53
  65. data/GPL +0 -340
  66. data/examples/metadata.xml +0 -42
  67. data/lib/rets4r/client/metadataindex.rb +0 -82
  68. data/lib/rets4r/client/parser.rb +0 -141
  69. data/lib/rets4r/client/parser/rexml.rb +0 -75
  70. data/lib/rets4r/client/parser/xmlparser.rb +0 -95
  71. data/test/client/parser/tc_rexml.rb +0 -17
  72. data/test/client/parser/tc_xmlparser.rb +0 -21
  73. data/test/client/tc_auth.rb +0 -68
  74. data/test/client/tc_client.rb +0 -320
  75. data/test/client/tc_metadataindex.rb +0 -36
  76. data/test/client/test_parser.rb +0 -128
  77. data/test/client/ts_all.rb +0 -8
  78. data/test/ts_all.rb +0 -1
  79. 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
- class Client
3
- class Transaction
4
- attr_accessor :reply_code, :reply_text, :response, :metadata, :header, :data, :maxrows,
5
- :count, :delimiter, :secondary_response
6
-
7
- def initialize
8
- self.maxrows = false
9
- self.header = []
10
- self.data = []
11
- self.delimiter = ?\t
12
- end
13
-
14
- def success?
15
- return true if self.reply_code == '0'
16
- return false
17
- end
18
-
19
- def has_data?
20
- return true if self.data.length > 0
21
- return false
22
- end
23
-
24
- def maxrows?
25
- return true if self.maxrows
26
- return false
27
- end
28
-
29
- def ascii_delimiter
30
- self.delimiter.chr
31
- end
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