jwulff-rets4r 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ = rets4r
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 John Wulff. See LICENSE for details.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 1
4
+ :patch: 1
@@ -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