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
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 John Wulff
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/VERSION.yml
ADDED
data/lib/rets4r/auth.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module RETS4R
|
4
|
+
class Auth
|
5
|
+
# This is the primary method that would normally be used, and while it
|
6
|
+
def Auth.authenticate(response, username, password, uri, method, requestId, useragent, nc = 0)
|
7
|
+
if response['www-authenticate'].blank?
|
8
|
+
raise "Missing required header 'www-authenticate'. Got: #{response}"
|
9
|
+
end
|
10
|
+
|
11
|
+
authHeader = Auth.parse_header(response['www-authenticate'])
|
12
|
+
|
13
|
+
cnonce = cnonce(useragent, password, requestId, authHeader['nonce'])
|
14
|
+
|
15
|
+
authHash = calculate_digest(username, password, authHeader['realm'], authHeader['nonce'], method, uri, authHeader['qop'], cnonce, nc)
|
16
|
+
|
17
|
+
header = ''
|
18
|
+
header << "Digest username=\"#{username}\", "
|
19
|
+
header << "realm=\"#{authHeader['realm']}\", "
|
20
|
+
header << "qop=\"#{authHeader['qop']}\", "
|
21
|
+
header << "uri=\"#{uri}\", "
|
22
|
+
header << "nonce=\"#{authHeader['nonce']}\", "
|
23
|
+
header << "nc=#{('%08x' % nc)}, "
|
24
|
+
header << "cnonce=\"#{cnonce}\", "
|
25
|
+
header << "response=\"#{authHash}\", "
|
26
|
+
header << "opaque=\"#{authHeader['opaque']}\""
|
27
|
+
|
28
|
+
return header
|
29
|
+
end
|
30
|
+
|
31
|
+
def Auth.calculate_digest(username, password, realm, nonce, method, uri, qop = false, cnonce = false, nc = 0)
|
32
|
+
a1 = "#{username}:#{realm}:#{password}"
|
33
|
+
a2 = "#{method}:#{uri}"
|
34
|
+
|
35
|
+
response = '';
|
36
|
+
|
37
|
+
requestId = Auth.request_id unless requestId
|
38
|
+
|
39
|
+
if (qop)
|
40
|
+
throw ArgumentException, 'qop requires a cnonce to be provided.' unless cnonce
|
41
|
+
|
42
|
+
response = Digest::MD5.hexdigest("#{Digest::MD5.hexdigest(a1)}:#{nonce}:#{('%08x' % nc)}:#{cnonce}:#{qop}:#{Digest::MD5.hexdigest(a2)}")
|
43
|
+
else
|
44
|
+
response = Digest::MD5.hexdigest("#{Digest::MD5.hexdigest(a1)}:#{nonce}:#{Digest::MD5.hexdigest(a2)}")
|
45
|
+
end
|
46
|
+
|
47
|
+
return response
|
48
|
+
end
|
49
|
+
|
50
|
+
def Auth.parse_header(header)
|
51
|
+
type = header[0, header.index(' ')]
|
52
|
+
args = header[header.index(' '), header.length].strip.split(',')
|
53
|
+
|
54
|
+
parts = {'type' => type}
|
55
|
+
|
56
|
+
args.each do |arg|
|
57
|
+
name, value = arg.split('=')
|
58
|
+
|
59
|
+
parts[name.downcase.strip] = value.tr('"', '').strip
|
60
|
+
end
|
61
|
+
|
62
|
+
return parts
|
63
|
+
end
|
64
|
+
|
65
|
+
def Auth.request_id
|
66
|
+
Digest::MD5.hexdigest(Time.new.to_f.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
def Auth.cnonce(useragent, password, requestId, nonce)
|
70
|
+
Digest::MD5.hexdigest("#{useragent}:#{password}:#{requestId}:#{nonce}")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -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,20 @@
|
|
1
|
+
module RETS4R
|
2
|
+
class Client
|
3
|
+
# Represents a RETS object (as returned by the get_object) transaction.
|
4
|
+
class DataObject
|
5
|
+
attr_accessor :type, :data
|
6
|
+
|
7
|
+
alias :info :type
|
8
|
+
|
9
|
+
def initialize(type, data)
|
10
|
+
self.type = type
|
11
|
+
self.data = data
|
12
|
+
end
|
13
|
+
|
14
|
+
def success?
|
15
|
+
return true if self.data
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
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,82 @@
|
|
1
|
+
module RETS4R
|
2
|
+
class Client
|
3
|
+
# Provides a means of indexing metadata to allow faster searching of it.
|
4
|
+
# This is in dire need of a review and cleanup, so I advise you not to use it until that has been done.
|
5
|
+
class MetadataIndex
|
6
|
+
# Constructor that takes an array of Metadata objects as its sole argument.
|
7
|
+
def initialize(metadata)
|
8
|
+
@metadata = metadata
|
9
|
+
@index = index(@metadata)
|
10
|
+
end
|
11
|
+
|
12
|
+
# WARNING! Not working properly (does NOT pass unit test)
|
13
|
+
# This is more of a free form search than #lookup, but it is also slower because it must iterate
|
14
|
+
# through the entire metadata array. This also returns multiple matches, which #lookup doesn't do.
|
15
|
+
def search(type, attributes = {})
|
16
|
+
matches = []
|
17
|
+
|
18
|
+
@metadata.each do |meta|
|
19
|
+
catch :mismatch do
|
20
|
+
if meta.type == type
|
21
|
+
attributes.each do |k,v|
|
22
|
+
throw :mismatch unless meta.attributes[k] == v
|
23
|
+
end
|
24
|
+
|
25
|
+
matches << meta
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
return matches
|
31
|
+
end
|
32
|
+
|
33
|
+
# This is a streamlined and probably the preferred method for looking up metadata because it
|
34
|
+
# uses a index to quickly access the data. The downside is that it is not very flexible. This also
|
35
|
+
# only returns one (the "best") match. If you need multiple matches, then you should use #search.
|
36
|
+
# Tests show about a 690% speed increase by using #lookup over #search, so you've been warned.
|
37
|
+
def lookup(type, *attributes)
|
38
|
+
key = type
|
39
|
+
key << attributes.join('')
|
40
|
+
|
41
|
+
@index[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Provided an array of metadata, it indexes it for faster lookup.
|
47
|
+
# Takes a +Metadata+ object as its argument.
|
48
|
+
def index(metadata)
|
49
|
+
index = {}
|
50
|
+
|
51
|
+
metadata.each do |meta|
|
52
|
+
key = generate_key(meta)
|
53
|
+
|
54
|
+
index[key] = meta
|
55
|
+
end
|
56
|
+
|
57
|
+
return index
|
58
|
+
end
|
59
|
+
|
60
|
+
# Used to generate the key for a specified piece of metadata. Takes a +Metadata+ object as its
|
61
|
+
# argument.
|
62
|
+
def generate_key(meta)
|
63
|
+
key = meta.type
|
64
|
+
|
65
|
+
case (meta.type)
|
66
|
+
when 'METADATA-LOOKUP'
|
67
|
+
key << meta.attributes['Resource']
|
68
|
+
when 'METADATA-LOOKUP_TYPE'
|
69
|
+
key << meta.attributes['Resource'] << meta.attributes['Lookup']
|
70
|
+
when 'METADATA-CLASS'
|
71
|
+
key << meta.attributes['Resource']
|
72
|
+
when 'METADATA-OBJECT'
|
73
|
+
key << meta.attributes['Resource']
|
74
|
+
when 'METADATA-TABLE'
|
75
|
+
key << meta.attributes['Resource'] << meta.attributes['Class']
|
76
|
+
end
|
77
|
+
|
78
|
+
return key
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# This is the generic parser
|
2
|
+
#
|
3
|
+
# Supports responses, data, metadata, reply codes, and reply text.
|
4
|
+
#
|
5
|
+
# Supports the following tags:
|
6
|
+
# RETS, METADATA-.*, MAXROWS, COLUMNS, DATA, COUNT, DELIMITER
|
7
|
+
#
|
8
|
+
# Metadata is built as:
|
9
|
+
# (Metadata 1-> data row
|
10
|
+
# -> data row),
|
11
|
+
# (Metadata 2 -> data row),
|
12
|
+
# etc.
|
13
|
+
#
|
14
|
+
# Data is built as:
|
15
|
+
# Data 1, Data 2, Data N
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# TODO
|
19
|
+
# Add comments/documentation
|
20
|
+
# Handle more tags (if any are unaccounted for)
|
21
|
+
# Handle standard (non-compact) output
|
22
|
+
# Case Insensitivity?
|
23
|
+
# There is still some holdovers from the previous organization of parsers, and they should be cleaned
|
24
|
+
# up at some point.
|
25
|
+
|
26
|
+
require 'cgi'
|
27
|
+
|
28
|
+
module RETS4R
|
29
|
+
class Client
|
30
|
+
module Parser
|
31
|
+
attr_accessor :output, :logger
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@transaction = Transaction.new
|
35
|
+
@current = []
|
36
|
+
@output = 2
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_transaction
|
40
|
+
@transaction
|
41
|
+
end
|
42
|
+
|
43
|
+
#### Stream Listener Events
|
44
|
+
def tag_start(name, attrs)
|
45
|
+
@current.push(name)
|
46
|
+
|
47
|
+
case name
|
48
|
+
when 'RETS'
|
49
|
+
@transaction.reply_code = attrs['ReplyCode']
|
50
|
+
@transaction.reply_text = attrs['ReplyText']
|
51
|
+
when /METADATA.*/
|
52
|
+
@metadata = Metadata.new(name)
|
53
|
+
@metadata.attributes = attrs
|
54
|
+
when 'MAXROWS'
|
55
|
+
@transaction.maxrows = true
|
56
|
+
when 'COUNT'
|
57
|
+
@transaction.count = attrs['Records'].to_i
|
58
|
+
when 'DELIMITER'
|
59
|
+
@transaction.delimiter = attrs['value'].to_i
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def text(text)
|
64
|
+
case @current[-1]
|
65
|
+
when 'COLUMNS'
|
66
|
+
@transaction.header = parse_compact_line(text, @transaction.ascii_delimiter)
|
67
|
+
|
68
|
+
when 'DATA'
|
69
|
+
if @transaction.header.length > 0
|
70
|
+
data_type << parse_data(text, @transaction.header)
|
71
|
+
else
|
72
|
+
data_type << parse_compact_line(text, @transaction.ascii_delimiter)
|
73
|
+
end
|
74
|
+
|
75
|
+
when 'RETS-RESPONSE'
|
76
|
+
@transaction.response = parse_key_value_body(text)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def tag_end(name)
|
81
|
+
@current.pop
|
82
|
+
|
83
|
+
@transaction.data << @metadata if name =~ /METADATA.*/
|
84
|
+
end
|
85
|
+
|
86
|
+
#### Helper Methods
|
87
|
+
def clean_xml(xml)
|
88
|
+
# This is a hack, and it assumes that we're using compact mode, but it's the easiest way to
|
89
|
+
# strip out those bad "<" and ">" characters that were not properly escaped...
|
90
|
+
xml.gsub!(/<DATA>(.*)<\/DATA>/i) do |match|
|
91
|
+
"<DATA>#{CGI::escapeHTML(CGI::unescapeHTML($1))}</DATA>"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def data_type
|
96
|
+
if @current[-2].index('METADATA') === 0
|
97
|
+
return @metadata
|
98
|
+
else
|
99
|
+
return @transaction.data
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_compact_line(data, delim = "\t")
|
104
|
+
begin
|
105
|
+
return data.to_s.strip.split(delim)
|
106
|
+
rescue
|
107
|
+
raise "Error while parsing compact line: #{$!} with data: #{data}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_data(data, header)
|
112
|
+
results = Data.new(@current[-2])
|
113
|
+
|
114
|
+
parsed_data = parse_compact_line(data, @transaction.ascii_delimiter)
|
115
|
+
|
116
|
+
header.length.times do |pos|
|
117
|
+
results[header[pos]] = parsed_data[pos]
|
118
|
+
end
|
119
|
+
|
120
|
+
results
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_key_value_body(data)
|
124
|
+
parsed = {}
|
125
|
+
|
126
|
+
data.each do |line|
|
127
|
+
(key, value) = line.strip.split('=')
|
128
|
+
parsed[key] = value
|
129
|
+
end
|
130
|
+
|
131
|
+
return parsed
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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].attributes['value'].to_i.chr
|
11
|
+
columns = doc.get_elements('/RETS/COLUMNS')[0]
|
12
|
+
rows = doc.get_elements('/RETS/DATA')
|
13
|
+
|
14
|
+
parse_data(columns, rows, delimiter)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_data(column_element, row_elements, delimiter = "\t")
|
18
|
+
|
19
|
+
column_names = column_element.text.split(delimiter)
|
20
|
+
|
21
|
+
result = []
|
22
|
+
|
23
|
+
data = row_elements.each do |data_row|
|
24
|
+
data_row = data_row.text.split(delimiter)
|
25
|
+
|
26
|
+
row_result = {}
|
27
|
+
|
28
|
+
column_names.each_with_index do |col, x|
|
29
|
+
row_result[col] = data_row[x]
|
30
|
+
end
|
31
|
+
|
32
|
+
row_result.reject! { |k,v| k.blank? }
|
33
|
+
|
34
|
+
result << row_result
|
35
|
+
end
|
36
|
+
|
37
|
+
return result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
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,103 @@
|
|
1
|
+
require 'rets4r/client/transaction'
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
module RETS4R
|
5
|
+
class Client
|
6
|
+
class ResponseParser
|
7
|
+
def parse_key_value(xml)
|
8
|
+
parse_common(xml) do |doc|
|
9
|
+
parsed = nil
|
10
|
+
first_child = doc.get_elements('/RETS/RETS-RESPONSE')[0]
|
11
|
+
unless first_child.nil?
|
12
|
+
parsed = {}
|
13
|
+
first_child.text.each do |line|
|
14
|
+
(key, value) = line.strip.split('=')
|
15
|
+
key.strip! if key
|
16
|
+
value.strip! if value
|
17
|
+
parsed[key] = value
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise 'Response was not a proper RETS XML doc!'
|
21
|
+
end
|
22
|
+
|
23
|
+
if parsed.nil?
|
24
|
+
raise "Response was not valid key/value format"
|
25
|
+
else
|
26
|
+
parsed
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_results(xml, format)
|
32
|
+
parse_common(xml) do |doc|
|
33
|
+
parser = get_parser_by_name(format)
|
34
|
+
parser.parse_results(doc)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_count(xml)
|
39
|
+
parse_common(xml) do |doc|
|
40
|
+
doc.get_elements('/RETS/COUNT')[0].attributes['Records']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_metadata(xml, format)
|
45
|
+
parse_common(xml) do |doc|
|
46
|
+
return REXML::Document.new(xml)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_object_response(xml)
|
51
|
+
parse_common(xml) do |doc|
|
52
|
+
# XXX
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def parse_common(xml, &block)
|
59
|
+
if xml == ''
|
60
|
+
raise RETSException, 'No transaction body was returned!'
|
61
|
+
end
|
62
|
+
|
63
|
+
File.open('/tmp/meh', 'w') do |file|
|
64
|
+
file.write(xml)
|
65
|
+
end
|
66
|
+
|
67
|
+
doc = REXML::Document.new(xml)
|
68
|
+
|
69
|
+
root = doc.root
|
70
|
+
if root.nil? || root.name != 'RETS'
|
71
|
+
raise "Response had invalid root node. Document was: #{doc.inspect}"
|
72
|
+
end
|
73
|
+
|
74
|
+
transaction = Transaction.new
|
75
|
+
transaction.reply_code = root.attributes['ReplyCode']
|
76
|
+
transaction.reply_text = root.attributes['ReplyText']
|
77
|
+
transaction.maxrows = (doc.get_elements('/RETS/MAXROWS').length > 0)
|
78
|
+
|
79
|
+
|
80
|
+
# XXX: If it turns out we need to parse the response of errors, then this will
|
81
|
+
# need to change.
|
82
|
+
if transaction.reply_code.to_i > 0
|
83
|
+
exception_type = Client::EXCEPTION_TYPES[transaction.reply_code.to_i] || RETSTransactionException
|
84
|
+
raise exception_type, "#{transaction.reply_code} - #{transaction.reply_text}"
|
85
|
+
end
|
86
|
+
|
87
|
+
transaction.response = yield doc
|
88
|
+
return transaction
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_parser_by_name(name)
|
92
|
+
case name
|
93
|
+
when 'COMPACT'
|
94
|
+
type = RETS4R::Client::CompactDataParser
|
95
|
+
else
|
96
|
+
raise "Invalid format #{name}"
|
97
|
+
end
|
98
|
+
type.new
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
@@ -0,0 +1,34 @@
|
|
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
|