rets4r 0.8.5 → 1.1.18
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/{test/client/data/1.5/metadata.xml → .gemtest} +0 -0
- data/CHANGELOG +611 -66
- data/CONTRIBUTORS +6 -2
- data/Gemfile +1 -0
- data/LICENSE +22 -0
- data/MANIFEST +63 -0
- data/NEWS +203 -0
- data/{README → README.rdoc} +11 -4
- data/RUBYS +7 -7
- data/Rakefile +48 -0
- data/TODO +5 -1
- data/examples/client_get_object.rb +31 -42
- data/examples/client_login.rb +20 -18
- data/examples/client_mapper.rb +17 -0
- data/examples/client_metadata.rb +28 -28
- data/examples/client_parser.rb +9 -0
- data/examples/client_search.rb +25 -27
- data/examples/settings.yml +114 -0
- data/lib/rets4r.rb +14 -1
- data/lib/rets4r/auth.rb +70 -66
- data/lib/rets4r/client.rb +470 -650
- data/lib/rets4r/client/data.rb +13 -13
- data/lib/rets4r/client/dataobject.rb +27 -19
- data/lib/rets4r/client/exceptions.rb +116 -0
- data/lib/rets4r/client/links.rb +32 -0
- data/lib/rets4r/client/metadata.rb +12 -12
- data/lib/rets4r/client/parsers/compact.rb +42 -0
- data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
- data/lib/rets4r/client/parsers/metadata.rb +92 -0
- data/lib/rets4r/client/parsers/response_parser.rb +100 -0
- data/lib/rets4r/client/requester.rb +143 -0
- data/lib/rets4r/client/transaction.rb +30 -33
- data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
- data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
- data/lib/rets4r/core_ext/hash/keys.rb +46 -0
- data/lib/rets4r/core_ext/hash/slice.rb +39 -0
- data/lib/rets4r/listing_mapper.rb +17 -0
- data/lib/rets4r/listing_service.rb +35 -0
- data/lib/rets4r/loader.rb +8 -0
- data/lib/tasks/annotations.rake +121 -0
- data/lib/tasks/coverage.rake +13 -0
- data/rets4r.gemspec +24 -0
- data/spec/rets4r_compact_data_parser_spec.rb +7 -0
- data/test/data/1.5/bad_compact.xml +7 -0
- data/test/data/1.5/count_only_compact.xml +3 -0
- data/test/{client/data → data}/1.5/error.xml +0 -0
- data/test/{client/data → data}/1.5/invalid_compact.xml +0 -0
- data/test/{client/data → data}/1.5/login.xml +0 -0
- data/test/data/1.5/metadata.xml +0 -0
- data/test/{client/data → data}/1.5/search_compact.xml +0 -0
- data/test/data/1.5/search_compact_big.xml +136 -0
- data/test/{client/data → data}/1.5/search_unescaped_compact.xml +0 -0
- data/test/data/listing_service.yml +36 -0
- data/test/test_auth.rb +68 -0
- data/test/test_client.rb +342 -0
- data/test/test_client_links.rb +39 -0
- data/test/test_compact_nokogiri.rb +64 -0
- data/test/test_helper.rb +12 -0
- data/test/test_listing_mapper.rb +112 -0
- data/test/test_loader.rb +24 -0
- data/test/test_parser.rb +96 -0
- data/test/test_quality.rb +57 -0
- metadata +168 -53
- data/GPL +0 -340
- data/examples/metadata.xml +0 -42
- data/lib/rets4r/client/metadataindex.rb +0 -82
- data/lib/rets4r/client/parser.rb +0 -141
- data/lib/rets4r/client/parser/rexml.rb +0 -75
- data/lib/rets4r/client/parser/xmlparser.rb +0 -95
- data/test/client/parser/tc_rexml.rb +0 -17
- data/test/client/parser/tc_xmlparser.rb +0 -21
- data/test/client/tc_auth.rb +0 -68
- data/test/client/tc_client.rb +0 -320
- data/test/client/tc_metadataindex.rb +0 -36
- data/test/client/test_parser.rb +0 -128
- data/test/client/ts_all.rb +0 -8
- data/test/ts_all.rb +0 -1
- data/test/ts_client.rb +0 -1
data/lib/rets4r/client/data.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
module RETS4R
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
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
|
@@ -1,20 +1,28 @@
|
|
1
1
|
module RETS4R
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
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
|
@@ -1,15 +1,15 @@
|
|
1
1
|
require 'rets4r/client/data'
|
2
2
|
|
3
3
|
module RETS4R
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
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
|