sean-rets 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ module Rets
2
+ class LockingHttpClient
3
+ def initialize(http_client, locker, lock_name, options={})
4
+ @http_client = http_client
5
+ @locker = locker
6
+ @lock_name = lock_name
7
+ @options = options
8
+ end
9
+
10
+ def http_get(url, params=nil, extra_headers={})
11
+ lock_around do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ lock_around do
18
+ @http_client.http_post(url, params, extra_headers)
19
+ end
20
+ end
21
+
22
+ def save_cookie_store(force=nil)
23
+ @http_client.save_cookie_store(force)
24
+ end
25
+
26
+ def lock_around(&block)
27
+ result = nil
28
+ @locker.lock(@lock_name, @options) do
29
+ result = block.call
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ module Rets
2
+ class MeasuringHttpClient
3
+ def initialize(http_client, stats, prefix)
4
+ @http_client = http_client
5
+ @stats = stats
6
+ @prefix = prefix
7
+ end
8
+
9
+ def http_get(url, params=nil, extra_headers={})
10
+ @stats.count("#{@prefix}.http_get_rate")
11
+ @stats.time("#{@prefix}.http_get") do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ @stats.count("#{@prefix}.http_post_rate")
18
+ @stats.time("#{@prefix}.http_post") do
19
+ @http_client.http_post(url, params, extra_headers)
20
+ end
21
+ end
22
+
23
+ def save_cookie_store(force=nil)
24
+ @http_client.save_cookie_store(force)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ require 'rets/metadata/containers'
2
+ require 'rets/metadata/lookup_type'
3
+ require 'rets/metadata/resource'
4
+ require 'rets/metadata/rets_class'
5
+ require 'rets/metadata/root'
6
+ require 'rets/metadata/table'
@@ -0,0 +1,84 @@
1
+ module Rets
2
+ module Metadata
3
+ #########################
4
+ # Basic representation of the underlying metadata. This models
5
+ # the structure of RETS metadata closely. The OO-representation
6
+ # uses this structure for its construction. External usage of
7
+ # this API should be discouraged in favor of the richer OO
8
+ # representation.
9
+ #
10
+ module Containers
11
+ class Container
12
+ attr_accessor :fragment
13
+
14
+ def self.uses(*fields)
15
+ fields.each do |field|
16
+ define_method(field) do
17
+ instance_variable_get("@#{field}") ||
18
+ instance_variable_set("@#{field}", extract(fragment, field.to_s.capitalize))
19
+ end
20
+ end
21
+ end
22
+
23
+ uses :date, :version
24
+
25
+ def initialize(fragment)
26
+ self.fragment = fragment
27
+ end
28
+
29
+ def extract(fragment, attr)
30
+ fragment.attr(attr)
31
+ end
32
+
33
+ end
34
+
35
+ class RowContainer < Container
36
+
37
+ attr_accessor :rows
38
+
39
+ def initialize(doc)
40
+ super
41
+ self.rows = Parser::Compact.parse_document(doc)
42
+ end
43
+
44
+ end
45
+
46
+ class ResourceContainer < RowContainer
47
+ alias resources rows
48
+ end
49
+
50
+ class ClassContainer < RowContainer
51
+ uses :resource
52
+
53
+ alias classes rows
54
+ end
55
+
56
+ class TableContainer < RowContainer
57
+ uses :resource, :class
58
+
59
+ alias tables rows
60
+ end
61
+
62
+ class LookupContainer < RowContainer
63
+ uses :resource
64
+
65
+ alias lookups rows
66
+ end
67
+
68
+ class LookupTypeContainer < RowContainer
69
+ uses :resource, :lookup
70
+
71
+ alias lookup_types rows
72
+ end
73
+
74
+ class ObjectContainer < RowContainer
75
+ uses :resource
76
+
77
+ alias objects rows
78
+ end
79
+
80
+ class SystemContainer < Container
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,17 @@
1
+ module Rets
2
+ module Metadata
3
+ class LookupType
4
+ attr_accessor :long_value, :short_value, :value
5
+
6
+ def initialize(lookup_type_fragment)
7
+ self.value = lookup_type_fragment["Value"]
8
+ self.short_value = lookup_type_fragment["ShortValue"]
9
+ self.long_value = lookup_type_fragment["LongValue"]
10
+ end
11
+
12
+ def print_tree
13
+ puts " #{long_value} -> #{value}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,84 @@
1
+ module Rets
2
+ module Metadata
3
+ class Resource
4
+ class MissingRetsClass < RuntimeError; end
5
+ attr_accessor :rets_classes
6
+ attr_accessor :lookup_types
7
+ attr_accessor :key_field
8
+
9
+ attr_accessor :id
10
+
11
+ def initialize(resource)
12
+ self.rets_classes = []
13
+ self.lookup_types = {}
14
+
15
+ self.id = resource["ResourceID"]
16
+ self.key_field = resource["KeyField"]
17
+ end
18
+
19
+ def self.find_lookup_containers(metadata, resource)
20
+ metadata[:lookup].select { |lc| lc.resource == resource.id }
21
+ end
22
+
23
+ def self.find_lookup_type_containers(metadata, resource, lookup_name)
24
+ metadata[:lookup_type].select { |ltc| ltc.resource == resource.id && ltc.lookup == lookup_name }
25
+ end
26
+
27
+ def self.find_rets_classes(metadata, resource)
28
+ class_container = metadata[:class].detect { |c| c.resource == resource.id }
29
+ if class_container.nil?
30
+ raise MissingRetsClass.new("No Metadata classes for #{resource.id}")
31
+ else
32
+ class_container.classes
33
+ end
34
+ end
35
+
36
+ def self.build_lookup_tree(resource, metadata)
37
+ lookup_types = Hash.new {|h, k| h[k] = Array.new }
38
+
39
+ find_lookup_containers(metadata, resource).each do |lookup_container|
40
+ lookup_container.lookups.each do |lookup_fragment|
41
+ lookup_name = lookup_fragment["LookupName"]
42
+
43
+ find_lookup_type_containers(metadata, resource, lookup_name).each do |lookup_type_container|
44
+
45
+ lookup_type_container.lookup_types.each do |lookup_type_fragment|
46
+ lookup_types[lookup_name] << LookupType.new(lookup_type_fragment)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ lookup_types
53
+ end
54
+
55
+ def self.build_classes(resource, metadata)
56
+ find_rets_classes(metadata, resource).map do |rets_class_fragment|
57
+ RetsClass.build(rets_class_fragment, resource, metadata)
58
+ end
59
+ end
60
+
61
+ def self.build(resource_fragment, metadata, logger)
62
+ resource = new(resource_fragment)
63
+
64
+ resource.lookup_types = build_lookup_tree(resource, metadata)
65
+ resource.rets_classes = build_classes(resource, metadata)
66
+ resource
67
+ rescue MissingRetsClass => e
68
+ logger.warn(e.message)
69
+ nil
70
+ end
71
+
72
+ def print_tree
73
+ puts "Resource: #{id} (Key Field: #{key_field})"
74
+
75
+ rets_classes.each(&:print_tree)
76
+ end
77
+
78
+ def find_rets_class(rets_class_name)
79
+ rets_classes.detect {|rc| rc.name == rets_class_name }
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,48 @@
1
+ module Rets
2
+ module Metadata
3
+ class RetsClass
4
+ attr_accessor :tables
5
+ attr_accessor :name
6
+ attr_accessor :visible_name
7
+ attr_accessor :description
8
+ attr_accessor :resource
9
+
10
+ def initialize(rets_class_fragment, resource)
11
+ self.resource = resource
12
+ self.tables = []
13
+ self.name = rets_class_fragment["ClassName"]
14
+ self.visible_name = rets_class_fragment["VisibleName"]
15
+ self.description = rets_class_fragment["Description"]
16
+ end
17
+
18
+ def self.find_table_container(metadata, resource, rets_class)
19
+ metadata[:table].detect { |t| t.resource == resource.id && t.class == rets_class.name }
20
+ end
21
+
22
+ def self.build(rets_class_fragment, resource, metadata)
23
+ rets_class = new(rets_class_fragment, resource)
24
+
25
+ table_container = find_table_container(metadata, resource, rets_class)
26
+
27
+ if table_container
28
+ table_container.tables.each do |table_fragment|
29
+ rets_class.tables << TableFactory.build(table_fragment, resource)
30
+ end
31
+ end
32
+
33
+ rets_class
34
+ end
35
+
36
+ def print_tree
37
+ puts " Class: #{name}"
38
+ puts " Visible Name: #{visible_name}"
39
+ puts " Description : #{description}"
40
+ tables.each(&:print_tree)
41
+ end
42
+
43
+ def find_table(name)
44
+ tables.detect { |value| value.name == name }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,152 @@
1
+ module Rets
2
+ module Metadata
3
+ METADATA_TYPES = %w(SYSTEM RESOURCE CLASS TABLE LOOKUP LOOKUP_TYPE OBJECT)
4
+
5
+ # It's useful when dealing with the Rets standard to represent their
6
+ # relatively flat namespace of interweived components as a Tree. With
7
+ # a collection of resources at the top, and their various, classes,
8
+ # tables, lookups, and lookup types underneath.
9
+ #
10
+ # It looks something like ...
11
+ #
12
+ # Resource
13
+ # |
14
+ # Class
15
+ # |
16
+ # `-- Table
17
+ # |
18
+ # `-- Lookups
19
+ # |
20
+ # `-- LookupType
21
+ #
22
+ # For our purposes it was helpful to denormalize some of the more deeply
23
+ # nested branches. In particular by relating Lookups to LookupTypes, and
24
+ # Tables to lookups with can simplify this diagram.
25
+ #
26
+ #
27
+ # Resource
28
+ # |
29
+ # Class
30
+ # |
31
+ # `-- Table
32
+ # |
33
+ # `-- Lookups
34
+ #
35
+ # By associating Tables and lookups when we parse this structure. It allows
36
+ # us to seemlessly map Lookup values to their Long or Short value forms.
37
+ class Root
38
+ # Metadata_types is the low level parsed representation of the raw xml
39
+ # sources. Just one level up, they contain Containers, consisting of
40
+ # SystemContainers or RowContainers
41
+ attr_writer :metadata_types
42
+
43
+ # the tree is the high level represenation of the metadata heiarchy
44
+ # it begins with root. Stored as a list of Metadata::Resources
45
+ attr_writer :tree
46
+
47
+ # Sources are the raw xml documents fetched for each metadata type
48
+ # they are stored as a hash with the type names as their keys
49
+ # and the raw xml as the values
50
+ attr_reader :sources
51
+
52
+ # Metadata can be unmarshalled from cache. @logger is not set during that process, constructor is not called.
53
+ # Client code must set it after unmarshalling.
54
+ attr_reader :logger
55
+
56
+ # fetcher is a proc that inverts control to the client to retrieve metadata
57
+ # types
58
+ def initialize(logger, sources)
59
+ @logger = logger
60
+ @tree = nil
61
+ @metadata_types = nil # TODO think up a better name ... containers?
62
+ @sources = sources
63
+ end
64
+
65
+ def marshal_dump
66
+ sources
67
+ end
68
+
69
+ def version
70
+ metadata_types[:system].first.version
71
+ end
72
+
73
+ def date
74
+ metadata_types[:system].first.date
75
+ end
76
+
77
+ # Wether there exists a more up to date version of the metadata to fetch
78
+ # is dependant on either a timestamp indicating when the most recent
79
+ # version was published, or a version number. These values may or may
80
+ # not exist on any given rets server.
81
+ def current?(current_timestamp, current_version)
82
+ if !current_version.to_s.empty? && !version.to_s.empty?
83
+ current_version == version
84
+ else
85
+ current_timestamp ? current_timestamp == date : true
86
+ end
87
+ end
88
+
89
+ def build_tree
90
+ tree = Hash.new { |h, k| h.key?(k.downcase) ? h[k.downcase] : nil }
91
+
92
+ resource_containers = metadata_types[:resource]
93
+
94
+ resource_containers.each do |resource_container|
95
+ resource_container.rows.each do |resource_fragment|
96
+ resource = Resource.build(resource_fragment, metadata_types, @logger)
97
+ #some mlses list resource types without an associated data, throw those away
98
+ tree[resource.id.downcase] = resource if resource
99
+ end
100
+ end
101
+
102
+ tree
103
+ end
104
+
105
+ def tree
106
+ @tree ||= build_tree
107
+ end
108
+
109
+ def print_tree
110
+ tree.each do |name, value|
111
+ value.print_tree
112
+ end
113
+ end
114
+
115
+ def metadata_types
116
+ return @metadata_types if @metadata_types
117
+
118
+ h = {}
119
+
120
+ sources.each do |name, source|
121
+ h[name.downcase.to_sym] = build_containers(Nokogiri.parse(source))
122
+ end
123
+
124
+ @metadata_types = h
125
+ end
126
+
127
+ # Returns an array of container classes that represents
128
+ # the metadata stored in the document provided.
129
+ def build_containers(doc)
130
+ # find all tags that match /RETS/METADATA-*
131
+ fragments = doc.xpath("/RETS/*[starts-with(name(), 'METADATA-')]")
132
+
133
+ fragments.map { |fragment| build_container(fragment) }
134
+ end
135
+
136
+ def build_container(fragment)
137
+ tag = fragment.name # METADATA-RESOURCE
138
+ type = tag.sub(/^METADATA-/, "") # RESOURCE
139
+
140
+ class_name = type.capitalize.gsub(/_(\w)/) { $1.upcase }
141
+ container_name = "#{class_name}Container"
142
+
143
+ if ::RUBY_VERSION < '1.9'
144
+ container_class = Containers.const_defined?(container_name) ? Containers.const_get(container_name) : Containers::Container
145
+ else
146
+ container_class = Containers.const_defined?(container_name, true) ? Containers.const_get(container_name, true) : Containers::Container
147
+ end
148
+ container_class.new(fragment)
149
+ end
150
+ end
151
+ end
152
+ end