jschairb-rets4r 1.1.18

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.
Files changed (63) hide show
  1. data/.document +5 -0
  2. data/CHANGELOG +566 -0
  3. data/CONTRIBUTORS +7 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +29 -0
  6. data/MANIFEST +62 -0
  7. data/NEWS +186 -0
  8. data/README.rdoc +43 -0
  9. data/RUBYS +56 -0
  10. data/Rakefile +50 -0
  11. data/TODO +35 -0
  12. data/examples/client_get_object.rb +49 -0
  13. data/examples/client_login.rb +39 -0
  14. data/examples/client_mapper.rb +17 -0
  15. data/examples/client_metadata.rb +42 -0
  16. data/examples/client_parser.rb +9 -0
  17. data/examples/client_search.rb +49 -0
  18. data/examples/settings.yml +114 -0
  19. data/lib/rets4r.rb +14 -0
  20. data/lib/rets4r/auth.rb +73 -0
  21. data/lib/rets4r/client.rb +487 -0
  22. data/lib/rets4r/client/data.rb +14 -0
  23. data/lib/rets4r/client/dataobject.rb +28 -0
  24. data/lib/rets4r/client/exceptions.rb +116 -0
  25. data/lib/rets4r/client/links.rb +32 -0
  26. data/lib/rets4r/client/metadata.rb +15 -0
  27. data/lib/rets4r/client/parsers/compact.rb +42 -0
  28. data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
  29. data/lib/rets4r/client/parsers/metadata.rb +92 -0
  30. data/lib/rets4r/client/parsers/response_parser.rb +100 -0
  31. data/lib/rets4r/client/requester.rb +143 -0
  32. data/lib/rets4r/client/transaction.rb +31 -0
  33. data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
  34. data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
  35. data/lib/rets4r/core_ext/hash/keys.rb +46 -0
  36. data/lib/rets4r/core_ext/hash/slice.rb +39 -0
  37. data/lib/rets4r/listing_mapper.rb +17 -0
  38. data/lib/rets4r/listing_service.rb +35 -0
  39. data/lib/rets4r/loader.rb +8 -0
  40. data/lib/tasks/annotations.rake +121 -0
  41. data/lib/tasks/coverage.rake +13 -0
  42. data/rets4r.gemspec +24 -0
  43. data/spec/rets4r_compact_data_parser_spec.rb +7 -0
  44. data/test/data/1.5/bad_compact.xml +7 -0
  45. data/test/data/1.5/count_only_compact.xml +3 -0
  46. data/test/data/1.5/error.xml +1 -0
  47. data/test/data/1.5/invalid_compact.xml +4 -0
  48. data/test/data/1.5/login.xml +16 -0
  49. data/test/data/1.5/metadata.xml +0 -0
  50. data/test/data/1.5/search_compact.xml +8 -0
  51. data/test/data/1.5/search_compact_big.xml +136 -0
  52. data/test/data/1.5/search_unescaped_compact.xml +8 -0
  53. data/test/data/listing_service.yml +36 -0
  54. data/test/test_auth.rb +68 -0
  55. data/test/test_client.rb +342 -0
  56. data/test/test_client_links.rb +39 -0
  57. data/test/test_compact_nokogiri.rb +64 -0
  58. data/test/test_helper.rb +12 -0
  59. data/test/test_listing_mapper.rb +112 -0
  60. data/test/test_loader.rb +24 -0
  61. data/test/test_parser.rb +96 -0
  62. data/test/test_quality.rb +57 -0
  63. metadata +211 -0
@@ -0,0 +1,14 @@
1
+ module RETS4R
2
+ class Client
3
+ # Represents a row of data. Nothing more than a glorfied Hash with a custom constructor and a
4
+ # type attribute.
5
+ class Data < ::Hash
6
+ attr_accessor :type
7
+
8
+ def initialize(type = false)
9
+ super
10
+ self.type = type
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ module RETS4R
2
+ class Client
3
+ class ObjectHeader < Hash
4
+ include Net::HTTPHeader
5
+ def initialize(raw_header)
6
+ initialize_http_header( raw_header )
7
+ end
8
+ end
9
+ # Represents a RETS object (as returned by the get_object) transaction.
10
+ class DataObject
11
+
12
+ attr_accessor :header, :data
13
+
14
+ alias :type :header
15
+ alias :info :type
16
+
17
+ def initialize(headers, data)
18
+ @header = ObjectHeader.new(headers)
19
+ @data = data
20
+ end
21
+
22
+ def success?
23
+ return true if self.data
24
+ return false
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,116 @@
1
+ module RETS4R
2
+ class Client
3
+ # This exception should be thrown when a generic client error is encountered.
4
+ class ClientException < Exception; end
5
+
6
+ # This exception should be thrown when there is an error with the parser, which is
7
+ # considered a subcomponent of the RETS client. It also includes the XML data that
8
+ # that was being processed at the time of the exception.
9
+ class ParserException < ClientException
10
+ attr_accessor :file
11
+ end
12
+
13
+ # The client does not currently support a specified action.
14
+ class Unsupported < ClientException; end
15
+
16
+ # The HTTP response returned by the server indicates that there was an error processing
17
+ # the request and the client cannot continue on its own without intervention.
18
+ class HTTPError < ClientException
19
+ attr_accessor :http_response
20
+
21
+ # Takes a HTTPResponse object
22
+ def initialize(http_response)
23
+ self.http_response = http_response
24
+ end
25
+
26
+ # Shorthand for calling HTTPResponse#code
27
+ def code
28
+ http_response.code
29
+ end
30
+
31
+ # Shorthand for calling HTTPResponse#message
32
+ def message
33
+ http_response.message
34
+ end
35
+
36
+ # Returns the RETS specification message for the HTTP response code
37
+ def rets_message
38
+ Client::RETS_HTTP_MESSAGES[code]
39
+ end
40
+
41
+ def to_s
42
+ "#{code} #{message}: #{rets_message}"
43
+ end
44
+ end
45
+
46
+ # A general RETS level exception was encountered. This would include HTTP and RETS
47
+ # specification level errors as well as informative mishaps such as authentication being
48
+ # required for access.
49
+ class RETSException < RuntimeError; end
50
+
51
+ # There was a problem with logging into the RETS server.
52
+ class LoginError < RETSException; end
53
+
54
+ # For internal client use only, it is thrown when the a RETS request is made but a password
55
+ # is prompted for.
56
+ class AuthRequired < RETSException; end
57
+
58
+ # A RETS transaction failed
59
+ class RETSTransactionException < RETSException; end
60
+
61
+ # Search Transaction Exceptions
62
+ class UnknownQueryFieldException < RETSTransactionException; end
63
+ class NoRecordsFoundException < RETSTransactionException; end
64
+ class InvalidSelectException < RETSTransactionException; end
65
+ class MiscellaneousSearchErrorException < RETSTransactionException; end
66
+ class InvalidQuerySyntaxException < RETSTransactionException; end
67
+ class UnauthorizedQueryException < RETSTransactionException; end
68
+ class MaximumRecordsExceededException < RETSTransactionException; end
69
+ class TimeoutException < RETSTransactionException; end
70
+ class TooManyOutstandingQueriesException < RETSTransactionException; end
71
+ class DTDVersionUnavailableException < RETSTransactionException; end
72
+
73
+ # GetObject Exceptions
74
+ class InvalidResourceException < RETSTransactionException; end
75
+ class InvalidTypeException < RETSTransactionException; end
76
+ class InvalidIdentifierException < RETSTransactionException; end
77
+ class NoObjectFoundException < RETSTransactionException; end
78
+ class UnsupportedMIMETypeException < RETSTransactionException; end
79
+ class UnauthorizedRetrievalException < RETSTransactionException; end
80
+ class ResourceUnavailableException < RETSTransactionException; end
81
+ class ObjectUnavailableException < RETSTransactionException; end
82
+ class RequestTooLargeException < RETSTransactionException; end
83
+ class TimeoutException < RETSTransactionException; end
84
+ class TooManyOutstandingRequestsException < RETSTransactionException; end
85
+ class MiscellaneousErrorException < RETSTransactionException; end
86
+
87
+ EXCEPTION_TYPES = {
88
+ # Search Transaction Reply Codes
89
+ 20200 => UnknownQueryFieldException,
90
+ 20201 => NoRecordsFoundException,
91
+ 20202 => InvalidSelectException,
92
+ 20203 => MiscellaneousSearchErrorException,
93
+ 20206 => InvalidQuerySyntaxException,
94
+ 20207 => UnauthorizedQueryException,
95
+ 20208 => MaximumRecordsExceededException,
96
+ 20209 => TimeoutException,
97
+ 20210 => TooManyOutstandingQueriesException,
98
+ 20514 => DTDVersionUnavailableException,
99
+
100
+ # GetObject Reply Codes
101
+ 20400 => InvalidResourceException,
102
+ 20401 => InvalidTypeException,
103
+ 20402 => InvalidIdentifierException,
104
+ 20403 => NoObjectFoundException,
105
+ 20406 => UnsupportedMIMETypeException,
106
+ 20407 => UnauthorizedRetrievalException,
107
+ 20408 => ResourceUnavailableException,
108
+ 20409 => ObjectUnavailableException,
109
+ 20410 => RequestTooLargeException,
110
+ 20411 => TimeoutException,
111
+ 20412 => TooManyOutstandingRequestsException,
112
+ 20413 => MiscellaneousErrorException
113
+
114
+ }
115
+ end
116
+ end
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+
3
+ module RETS4R #:nodoc:
4
+ class Client #:nodoc:
5
+ class Links < Hash
6
+ attr_accessor :logger
7
+ def self.from_login_url(login_url)
8
+ links = self.new
9
+ links['Login'] = URI.parse(login_url)
10
+ links
11
+ end
12
+ def login
13
+ self['Login']
14
+ end
15
+ def logout
16
+ self['Logout']
17
+ end
18
+ def metadata
19
+ self['GetMetadata']
20
+ end
21
+ def objects
22
+ self['GetObject']
23
+ end
24
+ def search
25
+ self['Search']
26
+ end
27
+ def action
28
+ self['Action']
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ require 'rets4r/client/data'
2
+
3
+ module RETS4R
4
+ class Client
5
+ # Represents a set of metadata. It is simply an extended Array with type and attributes accessors.
6
+ class Metadata < Array
7
+ attr_accessor :type, :attributes
8
+
9
+ def initialize(type = false)
10
+ self.type = type if type
11
+ self.attributes = {}
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # Parses XML response containing 'COMPACT' data format.
2
+
3
+ require 'cgi'
4
+
5
+ module RETS4R
6
+ class Client
7
+ class CompactDataParser
8
+ def parse_results(doc)
9
+
10
+ delimiter = doc.get_elements('/RETS/DELIMITER')[0] &&
11
+ doc.get_elements('/RETS/DELIMITER')[0].attributes['value'].to_i.chr
12
+ columns = doc.get_elements('/RETS/COLUMNS')[0]
13
+ rows = doc.get_elements('/RETS/DATA')
14
+
15
+ parse_data(columns, rows, delimiter)
16
+ end
17
+
18
+ def parse_data(column_element, row_elements, delimiter = "\t")
19
+
20
+ column_names = column_element.to_s.split(delimiter)
21
+
22
+ result = []
23
+
24
+ data = row_elements.each do |data_row|
25
+ data_row = data_row.text.split(delimiter)
26
+
27
+ row_result = {}
28
+
29
+ column_names.each_with_index do |col, x|
30
+ row_result[col] = data_row[x]
31
+ end
32
+
33
+ row_result.reject! { |k,v| k.nil? || k.empty? }
34
+
35
+ result << row_result
36
+ end
37
+
38
+ return result
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,91 @@
1
+ require 'nokogiri'
2
+ module RETS4R
3
+ class Client
4
+ class CompactNokogiriParser
5
+ include Enumerable
6
+ def initialize(io)
7
+ @doc = CompactDocument.new
8
+ @parser = Nokogiri::XML::SAX::Parser.new(@doc)
9
+ @io = io
10
+ end
11
+
12
+ def to_a
13
+ @parser.parse(@io) if @doc.results.empty?
14
+ @doc.results
15
+ end
16
+
17
+ def each(&block)
18
+ @doc.proc = block.to_proc
19
+ @parser.parse(@io)
20
+ nil
21
+ end
22
+
23
+ class CompactDocument < Nokogiri::XML::SAX::Document
24
+ attr_reader :results
25
+ attr_writer :proc
26
+
27
+ def initialize
28
+ @results = []
29
+ end
30
+ def start_element name, attrs = []
31
+ case name
32
+ when 'DELIMITER'
33
+ @delimiter = attrs.last.to_i.chr
34
+ when 'COLUMNS'
35
+ @columns_element = true
36
+ @string = ''
37
+ when 'DATA'
38
+ @data_element = true
39
+ @string = ''
40
+ when 'RETS'
41
+ handle_body_start attrs
42
+ end
43
+ end
44
+
45
+ def end_element name
46
+ case name
47
+ when 'COLUMNS'
48
+ @columns_element = false
49
+ @columns = @string.split(@delimiter)
50
+ when 'DATA'
51
+ @data_element = false
52
+ handle_row
53
+ end
54
+ end
55
+
56
+ def characters string
57
+ if @columns_element
58
+ @string << string
59
+ elsif @data_element
60
+ @string << string
61
+ elsif @reply_code
62
+ throw string
63
+ @reply_code = false
64
+ end
65
+ end
66
+
67
+ private
68
+ def handle_row
69
+ data = make_hash
70
+ if @proc
71
+ @proc.call(data)
72
+ else
73
+ @results << data
74
+ end
75
+ end
76
+ def handle_body_start attrs
77
+ attrs = Hash[*attrs]
78
+ if exception_class = Client::EXCEPTION_TYPES[attrs['ReplyCode'].to_i]
79
+ raise exception_class.new(attrs['ReplyText'])
80
+ end
81
+ end
82
+ def make_hash
83
+ @columns.zip(@string.split(@delimiter)).inject({}) do | h,(k,v) |
84
+ h[k] = v unless k.empty?
85
+ next h
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,92 @@
1
+ require 'rexml/document'
2
+ require 'yaml'
3
+ require 'rets4r/client/parsers/compact'
4
+
5
+ module RETS4R
6
+ class Client
7
+ class MetadataParser
8
+
9
+ TAGS = [ 'METADATA-RESOURCE',
10
+ 'METADATA-CLASS',
11
+ 'METADATA-TABLE',
12
+ 'METADATA-OBJECT',
13
+ 'METADATA-LOOKUP',
14
+ 'METADATA-LOOKUP_TYPE' ]
15
+
16
+ def initialize()
17
+ @parser = RETS4R::Client::CompactDataParser.new
18
+ end
19
+
20
+ def parse_file(file = 'metadata.xml')
21
+ xml = File.read(file)
22
+ doc = REXML::Document.new(xml)
23
+ parse(doc)
24
+ end
25
+
26
+ def parse(doc)
27
+
28
+ rets_resources = {}
29
+
30
+ doc.get_elements('/RETS/*').each do |elem|
31
+
32
+ next unless TAGS.include?(elem.name)
33
+
34
+ columns = elem.get_elements('COLUMNS')[0]
35
+ rows = elem.get_elements('DATA')
36
+
37
+ data = @parser.parse_data(columns, rows)
38
+
39
+ resource_id = elem.attributes['Resource']
40
+
41
+ case elem.name
42
+ when 'METADATA-RESOURCE'
43
+ data.each do |resource_info|
44
+ id = resource_info.delete('ResourceID')
45
+ rets_resources[id] = resource_info
46
+ end
47
+
48
+ when 'METADATA-CLASS'
49
+ data.each do |class_info|
50
+ class_name = class_info.delete('ClassName')
51
+ rets_resources[resource_id][:classes] ||= {}
52
+ rets_resources[resource_id][:classes][class_name] = class_info
53
+ end
54
+
55
+ when 'METADATA-TABLE'
56
+ class_name = elem.attributes['Class']
57
+ data.each do |table_info|
58
+ system_name = table_info.delete('SystemName')
59
+ rets_resources[resource_id][:classes][class_name][:tables] ||= {}
60
+ rets_resources[resource_id][:classes][class_name][:tables][system_name] = table_info
61
+ end
62
+
63
+ when 'METADATA-OBJECT'
64
+ data.each do |object_info|
65
+ object_type = object_info.delete('ObjectType')
66
+ rets_resources[resource_id][:objects] ||= {}
67
+ rets_resources[resource_id][:objects][object_type] = object_info
68
+ end
69
+
70
+ when 'METADATA-LOOKUP'
71
+ data.each do |lookup_info|
72
+ lookup_name = lookup_info.delete('LookupName')
73
+ rets_resources[resource_id][:lookups] ||= {}
74
+ rets_resources[resource_id][:lookups][lookup_name] = lookup_info
75
+ end
76
+
77
+ when 'METADATA-LOOKUP_TYPE'
78
+ lookup = elem.attributes['Lookup']
79
+ rets_resources[resource_id][:lookup_types] ||= {}
80
+ rets_resources[resource_id][:lookup_types][lookup] = {}
81
+ data.each do |lookup_type_info|
82
+ value = lookup_type_info.delete('Value')
83
+ rets_resources[resource_id][:lookup_types][lookup][value] = lookup_type_info
84
+ end
85
+ end
86
+ end
87
+
88
+ rets_resources
89
+ end
90
+ end
91
+ end
92
+ end
@@ -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
+