rets4r 0.8.2

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.
@@ -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,75 @@
1
+ require 'rets4r/client/parser'
2
+ require 'rexml/parsers/baseparser'
3
+ require 'rexml/parsers/streamparser'
4
+ require 'rexml/streamlistener'
5
+ require 'rets4r/client/transaction'
6
+ require 'rets4r/client/data'
7
+ require 'rets4r/client/metadata'
8
+
9
+ module RETS4R
10
+ class Client
11
+ module Parser
12
+ class REXML
13
+ include Parser
14
+
15
+ SUPPORTED_PARSERS << self
16
+
17
+ attr_accessor :logger
18
+
19
+ def initialize
20
+ @transaction = Transaction.new
21
+ @current = []
22
+ end
23
+
24
+ def parse(xml, output = false, do_retry = true)
25
+ output = self.output unless output # Allow per-parse output changes
26
+
27
+ return xml if output == OUTPUT_RAW
28
+
29
+ # This is an legacy option that is not currently supported by XMLParser, but it is left in
30
+ # here for reference or "undocumented usage."
31
+ if output == OUTPUT_DOM
32
+ return ::REXML::Document.new(xml)
33
+ end
34
+
35
+ # If we're here, then we need to output a RETS::Data object
36
+ listener = StreamListener.new
37
+
38
+ begin
39
+ stream = ::REXML::Parsers::StreamParser.new(xml, listener)
40
+ stream.parse
41
+ rescue ::REXML::ParseException, Exception
42
+ # We should get fancier here and actually check the type of error, but, err, oh well.
43
+ if do_retry
44
+ logger.info("Unable to parse XML on first try due to '#{$!}'. Now retrying.") if logger
45
+
46
+ return parse(clean_xml(xml), output, false)
47
+ else
48
+ ex = ParserException.new($!)
49
+ ex.file = xml
50
+
51
+ logger.error("REXML parser was unable to parse XML: #{$!}") if logger
52
+ logger.error("Unparsable XML was:\n#{xml}") if logger
53
+
54
+ raise ex
55
+ end
56
+ end
57
+
58
+ return listener.get_transaction
59
+ end
60
+
61
+ class StreamListener
62
+ include ::REXML::StreamListener
63
+ include Parser
64
+
65
+ def initialize(logger = nil)
66
+ self.logger = logger
67
+ @transaction = Transaction.new
68
+ @current = []
69
+ @output = 2
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,95 @@
1
+ # Because XMLParser may not be present on this system, we attempt to require it and if it
2
+ # succeeds, we create the XMLParser and add it to the supported parsers. The user of the client
3
+ # can still switch to REXML if desired.
4
+ begin
5
+ require 'xml/parser'
6
+ require 'rets4r/client/parser'
7
+ require 'rets4r/client/transaction'
8
+ require 'rets4r/client/data'
9
+ require 'rets4r/client/metadata'
10
+
11
+ module RETS4R
12
+ class Client
13
+ module Parser
14
+ class XMLParser < XML::Parser
15
+ include Parser
16
+
17
+ SUPPORTED_PARSERS << self
18
+
19
+ attr_accessor :logger
20
+
21
+ def initialize
22
+ @transaction = Transaction.new
23
+ @current = []
24
+ @text = ''
25
+ end
26
+
27
+ def parse(xml, output = false, do_retry = true)
28
+ begin
29
+ super(xml)
30
+ rescue XMLParserError
31
+ line = self.line
32
+
33
+ # We should get fancier here and actually check the type of error, but, err, oh well.
34
+ if do_retry
35
+ # We probably received this error because somebody forgot to escape XML entities...
36
+ # so we attempt to escape them ourselves...
37
+ do_retry = false
38
+
39
+ begin
40
+ cleaned_xml = self.clean_xml(xml)
41
+
42
+ # Because a cparser can only be used once per instantiation...
43
+ retry_xml = self.class.new
44
+ retry_xml.parse(cleaned_xml, output, do_retry)
45
+
46
+ @transaction = retry_xml.get_transaction
47
+
48
+ rescue
49
+ ex = ParserException.new($!)
50
+ ex.file = xml
51
+
52
+ raise ex
53
+ end
54
+ else
55
+ # We should never get here! But if we do...
56
+ raise "You really shouldn't be seeing this error message! This means that there was an unexpected error: #{$!} (#{$!.class})"
57
+ end
58
+ end
59
+
60
+ @transaction
61
+ end
62
+
63
+ def get_transaction
64
+ @transaction
65
+ end
66
+
67
+ private
68
+
69
+ #### Stream Listener Events
70
+ def startElement(name, attrs)
71
+ tag_start(name, attrs)
72
+ end
73
+
74
+ def character(text)
75
+ @text += text
76
+ end
77
+
78
+ def processText()
79
+ text(@text)
80
+
81
+ @text = ''
82
+ end
83
+
84
+ def endElement(name)
85
+ processText()
86
+
87
+ tag_end(name)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ rescue LoadError
94
+ # CParser is not available because we could not load the XMLParser library
95
+ end
@@ -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