jwulff-rets4r 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|