hyperactive 0.1.0
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.
- data/GPL-2 +339 -0
- data/README +33 -0
- data/lib/hyperactive/hooker.rb +63 -0
- data/lib/hyperactive/record.rb +358 -0
- data/lib/hyperactive/tree.rb +216 -0
- data/lib/hyperactive.rb +6 -0
- data/tests/record_test.rb +158 -0
- data/tests/test_helper.rb +66 -0
- data/tests/tree_benchmark.rb +66 -0
- data/tests/tree_test.rb +75 -0
- metadata +65 -0
@@ -0,0 +1,358 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'archipelago'
|
4
|
+
|
5
|
+
#
|
6
|
+
# A utility module to provide the functionality required for example
|
7
|
+
# if you want to use archipelago in a Ruby on Rails project *hint hint*.
|
8
|
+
#
|
9
|
+
module Hyperactive
|
10
|
+
|
11
|
+
#
|
12
|
+
# The default database connector.
|
13
|
+
#
|
14
|
+
CAPTAIN = Archipelago::Pirate::Captain.new
|
15
|
+
|
16
|
+
#
|
17
|
+
# A tiny <b>call</b>able class that saves stuff in
|
18
|
+
# indexes depending on certain attributes.
|
19
|
+
#
|
20
|
+
class IndexBuilder
|
21
|
+
#
|
22
|
+
# Get the first part of the key, that depends on the +attributes+.
|
23
|
+
#
|
24
|
+
def self.get_attribute_key_part(attributes)
|
25
|
+
"Hyperactive::IndexBuilder::#{attributes.join(",")}"
|
26
|
+
end
|
27
|
+
#
|
28
|
+
# Get the last part of the key, that depends on the +values+.
|
29
|
+
#
|
30
|
+
def self.get_value_key_part(values)
|
31
|
+
"#{values.join(",")}"
|
32
|
+
end
|
33
|
+
#
|
34
|
+
# Initialize an IndexBuilder giving it an array of +attributes+
|
35
|
+
# that will be indexed.
|
36
|
+
#
|
37
|
+
def initialize(attributes)
|
38
|
+
@attributes = attributes
|
39
|
+
end
|
40
|
+
#
|
41
|
+
# Get the Tree for the given +record+.
|
42
|
+
#
|
43
|
+
def get_tree_for(record)
|
44
|
+
values = @attributes.collect do |att|
|
45
|
+
if record.respond_to?(att)
|
46
|
+
record.send(att)
|
47
|
+
else
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
key = "#{self.class.get_attribute_key_part(@attributes)}::#{self.class.get_value_key_part(values)}"
|
52
|
+
return CAPTAIN[key] ||= Tree.get_instance
|
53
|
+
end
|
54
|
+
#
|
55
|
+
# Call this IndexBuilder and pass it a +block+.
|
56
|
+
#
|
57
|
+
# If the +argument+ is an Array then we know we are a save hook,
|
58
|
+
# otherwise we are a destroy hook.
|
59
|
+
#
|
60
|
+
def call(argument, &block)
|
61
|
+
yield
|
62
|
+
|
63
|
+
#
|
64
|
+
# If the argument is an Array (of old value, new value)
|
65
|
+
# then we are a save hook, otherwise a destroy hook.
|
66
|
+
#
|
67
|
+
if Array === argument
|
68
|
+
record = argument.last
|
69
|
+
old_record = argument.first
|
70
|
+
get_tree_for(old_record).delete(record.record_id)
|
71
|
+
get_tree_for(record)[record.record_id] = record
|
72
|
+
else
|
73
|
+
record = argument
|
74
|
+
get_tree_for(record).delete(record.record_id)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# A tiny <b>call</b>able class that saves stuff inside containers
|
81
|
+
# if they match certain criteria.
|
82
|
+
#
|
83
|
+
class MatchSaver
|
84
|
+
#
|
85
|
+
# Initialize this MatchSaver with a +key+, a <b>call</b>able +matcher+
|
86
|
+
# and a +mode+ (:select, :reject, :delete_if_match or :delete_unless_match).
|
87
|
+
#
|
88
|
+
def initialize(key, matcher, mode)
|
89
|
+
@key = key
|
90
|
+
@matcher = matcher
|
91
|
+
@mode = mode
|
92
|
+
end
|
93
|
+
#
|
94
|
+
# Depending on <i>@mode</i> and return value of <i>@matcher</i>.call
|
95
|
+
# may save record in the Hash-like container named <i>@key</i> in
|
96
|
+
# the main database after having yielded to +block+.
|
97
|
+
#
|
98
|
+
def call(argument, &block)
|
99
|
+
yield
|
100
|
+
|
101
|
+
record = argument
|
102
|
+
record = argument.last if Array === argument
|
103
|
+
|
104
|
+
case @mode
|
105
|
+
when :select
|
106
|
+
if @matcher.call(record)
|
107
|
+
CAPTAIN[@key][record.record_id] = record
|
108
|
+
else
|
109
|
+
CAPTAIN[@key].delete(record.record_id)
|
110
|
+
end
|
111
|
+
when :reject
|
112
|
+
if @matcher.call(record)
|
113
|
+
CAPTAIN[@key].delete(record.record_id)
|
114
|
+
else
|
115
|
+
CAPTAIN[@key][record.record_id] = record
|
116
|
+
end
|
117
|
+
when :delete_if_match
|
118
|
+
if @matcher.call(record)
|
119
|
+
CAPTAIN[@key].delete(record.record_id)
|
120
|
+
end
|
121
|
+
when :delete_unless_match
|
122
|
+
unless @matcher.call(record)
|
123
|
+
CAPTAIN[@key].delete(record.record_id)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# A convenient base class to inherit when you want the basic utility methods
|
131
|
+
# provided by for example ActiveRecord::Base *hint hint*.
|
132
|
+
#
|
133
|
+
# NB: After an instance is created, it will actually return a copy <b>within your local machine</b>
|
134
|
+
# which is not what is usually the case. Every other time you fetch it using a select or other
|
135
|
+
# method you will instead receive a proxy object to the database. This means that nothing you
|
136
|
+
# do to it at that point will be persistent or even necessarily have a defined result.
|
137
|
+
# Therefore: do not use <b>MyRecordSubclass.new</b> to <b>MyRecordSubclass#initialize</b> objects,
|
138
|
+
# instead use <b>MyRecordSubclass.get_instance</b>, since it will return a fresh proxy object
|
139
|
+
# instead of the devious original:
|
140
|
+
# my_instance = MyRecordSubclass.get_instance(*the_same_arguments_as_to_initialize)
|
141
|
+
#
|
142
|
+
class Record
|
143
|
+
|
144
|
+
@@create_hooks_by_class = {}
|
145
|
+
@@destroy_hooks_by_class = {}
|
146
|
+
@@save_hooks_by_class = {}
|
147
|
+
|
148
|
+
#
|
149
|
+
# The host we are running on.
|
150
|
+
#
|
151
|
+
HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
|
152
|
+
|
153
|
+
#
|
154
|
+
# Call this if you want to change the default database connector
|
155
|
+
# to something else.
|
156
|
+
#
|
157
|
+
def self.setup(options = {})
|
158
|
+
CAPTAIN.setup(options[:pirate_options])
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Return our create_hooks, which can then be treated as any old Array.
|
163
|
+
#
|
164
|
+
# These must be <b>call</b>able objects with an arity of 1
|
165
|
+
# that will be sent the instance about to be created (initial
|
166
|
+
# insertion into the database system) that take a block argument.
|
167
|
+
#
|
168
|
+
# The block argument will be a Proc that actually injects the
|
169
|
+
# instance into the database system.
|
170
|
+
#
|
171
|
+
# Use this to preprocess, validate and/or postprocess your
|
172
|
+
# instances upon creation.
|
173
|
+
#
|
174
|
+
def self.create_hooks
|
175
|
+
self.get_hook_array_by_class(@@create_hooks_by_class)
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Return our destroy_hooks, which can then be treated as any old Array.
|
180
|
+
#
|
181
|
+
# These must be <b>call</b>able objects with an arity of 1
|
182
|
+
# that will be sent the instance about to be destroyed (removal
|
183
|
+
# from the database system) that take a block argument.
|
184
|
+
#
|
185
|
+
# The block argument will be a Proc that actually removes the
|
186
|
+
# instance from the database system.
|
187
|
+
#
|
188
|
+
# Use this to preprocess, validate and/or postprocess your
|
189
|
+
# instances upon destruction.
|
190
|
+
#
|
191
|
+
def self.destroy_hooks
|
192
|
+
self.get_hook_array_by_class(@@destroy_hooks_by_class)
|
193
|
+
end
|
194
|
+
|
195
|
+
#
|
196
|
+
# Return our save_hooks, which can then be treated as any old Array.
|
197
|
+
#
|
198
|
+
# These must be <b>call</b>able objects with an arity of 1
|
199
|
+
# that will be sent [the old version, the new version] of the
|
200
|
+
# instance about to be saved (storage into the database system)
|
201
|
+
# along with a block argument.
|
202
|
+
#
|
203
|
+
# The block argument will be a Proc that actually saves the
|
204
|
+
# instance into the database system.
|
205
|
+
#
|
206
|
+
# Use this to preprocess, validate and/or postprocess your
|
207
|
+
# instances upon saving.
|
208
|
+
#
|
209
|
+
def self.save_hooks
|
210
|
+
self.get_hook_array_by_class(@@save_hooks_by_class)
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.index_by(*attributes)
|
214
|
+
attribute_key_part = IndexBuilder.get_attribute_key_part(attributes)
|
215
|
+
self.class_eval <<END
|
216
|
+
def self.find_by_#{attributes.join("_and_")}(*args)
|
217
|
+
key = "#{attribute_key_part}::" + IndexBuilder.get_value_key_part(args)
|
218
|
+
CAPTAIN[key]
|
219
|
+
end
|
220
|
+
END
|
221
|
+
index_builder = IndexBuilder.new(attributes)
|
222
|
+
self.save_hooks << index_builder
|
223
|
+
self.destroy_hooks << index_builder
|
224
|
+
end
|
225
|
+
|
226
|
+
#
|
227
|
+
# Will define a method called +name+ that will include all
|
228
|
+
# existing instances of this class that when sent to +matcher+.call
|
229
|
+
# return true. Will only return instances saved after this selector
|
230
|
+
# is defined.
|
231
|
+
#
|
232
|
+
def self.select(name, matcher)
|
233
|
+
key = self.collection_key(name)
|
234
|
+
CAPTAIN[key] ||= Tree.get_instance
|
235
|
+
self.class_eval <<END
|
236
|
+
def self.#{name}
|
237
|
+
CAPTAIN["#{key}"]
|
238
|
+
end
|
239
|
+
END
|
240
|
+
self.save_hooks << MatchSaver.new(key, matcher, :select)
|
241
|
+
self.destroy_hooks << MatchSaver.new(key, matcher, :delete_if_match)
|
242
|
+
end
|
243
|
+
|
244
|
+
#
|
245
|
+
# Will define a method called +name+ that will include all
|
246
|
+
# existing instances of this class that when sent to +matcher+.call
|
247
|
+
# does not return true. Will only return instances saved after this
|
248
|
+
# rejector is defined.
|
249
|
+
#
|
250
|
+
def self.reject(name, matcher)
|
251
|
+
key = self.collection_key(name)
|
252
|
+
CAPTAIN[key] ||= Tree.get_instance
|
253
|
+
self.class_eval <<END
|
254
|
+
def self.#{name}
|
255
|
+
CAPTAIN["#{key}"]
|
256
|
+
end
|
257
|
+
END
|
258
|
+
self.save_hooks << MatchSaver.new(key, matcher, :reject)
|
259
|
+
self.destroy_hooks << MatchSaver.new(key, matcher, :delete_unless_match)
|
260
|
+
end
|
261
|
+
|
262
|
+
#
|
263
|
+
# Return the record with +record_id+.
|
264
|
+
#
|
265
|
+
def self.find(record_id)
|
266
|
+
CAPTAIN[record_id]
|
267
|
+
end
|
268
|
+
|
269
|
+
#
|
270
|
+
# Will execute +block+ within a transaction.
|
271
|
+
#
|
272
|
+
def self.transaction(&block)
|
273
|
+
CAPTAIN.transaction(&block)
|
274
|
+
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# Use this method to get new instances of this class, since it will actually
|
278
|
+
# make sure it both resides in the database and return a proxy to the remote
|
279
|
+
# object.
|
280
|
+
#
|
281
|
+
def self.get_instance(*arguments)
|
282
|
+
instance = self.new(*arguments)
|
283
|
+
instance.instance_eval do
|
284
|
+
@record_id = Digest::SHA1.hexdigest("#{HOST}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}")
|
285
|
+
end
|
286
|
+
|
287
|
+
Hyperactive::Hooker.call_with_hooks(instance, *self.create_hooks) do
|
288
|
+
CAPTAIN[instance.record_id] = instance
|
289
|
+
end
|
290
|
+
|
291
|
+
proxy = CAPTAIN[instance.record_id]
|
292
|
+
|
293
|
+
return proxy
|
294
|
+
end
|
295
|
+
|
296
|
+
#
|
297
|
+
# Our semi-unique id.
|
298
|
+
#
|
299
|
+
attr_reader :record_id
|
300
|
+
|
301
|
+
#
|
302
|
+
# This will allow us to wrap any write of us to persistent storage
|
303
|
+
# in the @@save_hooks as long as the Archipelago::Hashish provider
|
304
|
+
# supports it. See Archipelago::Hashish::BerkeleyHashish for an example
|
305
|
+
# of Hashish providers that do this.
|
306
|
+
#
|
307
|
+
def save_hook(old_value, &block)
|
308
|
+
Hyperactive::Hooker.call_with_hooks([old_value, self], *self.class.save_hooks) do
|
309
|
+
yield
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
#
|
314
|
+
# Remove this instance from the database calling all the right hooks.
|
315
|
+
#
|
316
|
+
# Freezes this instance after having deleted it.
|
317
|
+
#
|
318
|
+
# Returns false without destroying anything if any of the @@pre_destroy_hooks
|
319
|
+
# returns false.
|
320
|
+
#
|
321
|
+
# Returns true otherwise.
|
322
|
+
#
|
323
|
+
def destroy
|
324
|
+
Hyperactive::Hooker.call_with_hooks(self, *self.class.destroy_hooks) do
|
325
|
+
CAPTAIN.delete(@record_id)
|
326
|
+
self.freeze
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
private
|
331
|
+
|
332
|
+
#
|
333
|
+
# The key used to store the collection with the given +sym+ as name.
|
334
|
+
#
|
335
|
+
def self.collection_key(sym)
|
336
|
+
"Hyperactive::Record::collection_key::#{sym}"
|
337
|
+
end
|
338
|
+
|
339
|
+
#
|
340
|
+
# Get an Array from +hash+ using <i>self</i> as
|
341
|
+
# key. If <i>self</i> doesnt exist in the +hash+
|
342
|
+
# it will recurse by calling the same method in the
|
343
|
+
# superclass until it has been called in Hyperactive::Record.
|
344
|
+
#
|
345
|
+
def self.get_hook_array_by_class(hash)
|
346
|
+
return hash[self] if hash.include?(self)
|
347
|
+
|
348
|
+
if self == Record
|
349
|
+
hash[self] = []
|
350
|
+
return self.get_hook_array_by_class(hash)
|
351
|
+
else
|
352
|
+
hash[self] = self.superclass.get_hook_array_by_class(hash).clone
|
353
|
+
return self.get_hook_array_by_class(hash)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
358
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'archipelago'
|
4
|
+
require 'rbtree'
|
5
|
+
|
6
|
+
module Hyperactive
|
7
|
+
|
8
|
+
#
|
9
|
+
# A class suitable for storing large and often-changing datasets in
|
10
|
+
# an Archipelago environment.
|
11
|
+
#
|
12
|
+
# Is constructed like a set of nested Hashes that automatically create
|
13
|
+
# new children on demand, and will thusly only have to check the path from
|
14
|
+
# the root node to the leaf for changes when method calls return (see Archipelago::Treasure::Chest)
|
15
|
+
# and will only have to actually store into database the leaf itself if it
|
16
|
+
# has changed.
|
17
|
+
#
|
18
|
+
class Tree < Record
|
19
|
+
|
20
|
+
attr_accessor :elements, :subtrees
|
21
|
+
|
22
|
+
WIDTH = 1 << 3
|
23
|
+
|
24
|
+
#
|
25
|
+
# Dont call this! Call <i>Tree.get_instance(options)</i> instead!
|
26
|
+
#
|
27
|
+
def initialize(options = {})
|
28
|
+
@width = options[:width] || WIDTH
|
29
|
+
@elements = {}
|
30
|
+
@subtrees = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Deletes +key+ in this Tree.
|
35
|
+
#
|
36
|
+
def delete(key)
|
37
|
+
if @elements
|
38
|
+
@elements.delete(key)
|
39
|
+
else
|
40
|
+
subtree_for(key).delete(key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Returns the size of this Tree.
|
46
|
+
#
|
47
|
+
def size
|
48
|
+
if @elements
|
49
|
+
@elements.size
|
50
|
+
else
|
51
|
+
@subtrees.t_collect do |tree_id, tree|
|
52
|
+
tree.size
|
53
|
+
end.inject(0) do |sum, size|
|
54
|
+
sum + size
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Returns all keys and values returning true for +callable+.call(key, value) in this Tree.
|
61
|
+
#
|
62
|
+
def select(callable)
|
63
|
+
if @elements
|
64
|
+
@elements.select do |k,v|
|
65
|
+
callable.call(k,v)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
@subtrees.t_collect do |tree_id, tree|
|
69
|
+
tree.select(callable)
|
70
|
+
end.inject([]) do |sum, match|
|
71
|
+
sum + match
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Returns all keys and values returning false for +callable+.call(key, value) in this Tree.
|
78
|
+
#
|
79
|
+
def reject(callable)
|
80
|
+
if @elements
|
81
|
+
@elements.reject do |k,v|
|
82
|
+
callable.call(k,v)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
@subtrees.t_collect do |tree_id, tree|
|
86
|
+
tree.reject(callable)
|
87
|
+
end.inject([]) do |sum, match|
|
88
|
+
sum + match
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Puts +value+ under +key+ in this Tree.
|
95
|
+
#
|
96
|
+
def []=(key, value)
|
97
|
+
if @elements
|
98
|
+
if @elements.size < @width
|
99
|
+
@elements[key] = value
|
100
|
+
else
|
101
|
+
split!
|
102
|
+
subtree_for(key)[key] = value
|
103
|
+
end
|
104
|
+
else
|
105
|
+
subtree_for(key)[key] = value
|
106
|
+
end
|
107
|
+
return value
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Returns the value for +key+ in this Tree.
|
112
|
+
#
|
113
|
+
def [](key)
|
114
|
+
if @elements
|
115
|
+
return @elements[key]
|
116
|
+
else
|
117
|
+
return subtree_for(key)[key]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Returns an Array containing +callable+.call(key, value) from all values in this Tree.
|
123
|
+
#
|
124
|
+
def collect(callable)
|
125
|
+
rval = []
|
126
|
+
self.each(Proc.new do |k,v|
|
127
|
+
rval << callable.call(k,v)
|
128
|
+
end)
|
129
|
+
return rval
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Does +callable+.call(key, value) on all values in this Tree.
|
134
|
+
#
|
135
|
+
def each(callable)
|
136
|
+
if @elements
|
137
|
+
@elements.each do |key, value|
|
138
|
+
callable.call(key, value)
|
139
|
+
end
|
140
|
+
else
|
141
|
+
@subtrees.t_each do |tree_id, tree|
|
142
|
+
tree.each(callable)
|
143
|
+
end
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Clear everything from this Tree and destroy it.
|
150
|
+
#
|
151
|
+
def destroy
|
152
|
+
if @elements
|
153
|
+
@elements.each do |key, value|
|
154
|
+
value.destroy if value.respond_to?(:destroy)
|
155
|
+
end
|
156
|
+
else
|
157
|
+
@subtrees.each do |tree_id, tree|
|
158
|
+
tree.destroy
|
159
|
+
end
|
160
|
+
end
|
161
|
+
freeze
|
162
|
+
super
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Clear everything from this Tree.
|
167
|
+
#
|
168
|
+
def clear
|
169
|
+
unless @elements
|
170
|
+
@subtrees.each do |tree_id, tree|
|
171
|
+
tree.clear
|
172
|
+
end
|
173
|
+
@subtrees = nil
|
174
|
+
end
|
175
|
+
@elements = {}
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
#
|
181
|
+
# Finds the subtree responsible for +key+.
|
182
|
+
#
|
183
|
+
# Does it in this ugly way cause the nice way of just doing modulo gave
|
184
|
+
# really odd results since all hashes seem to give some modulo values
|
185
|
+
# a lot more often than expected.
|
186
|
+
#
|
187
|
+
def subtree_for(key)
|
188
|
+
key_id = Digest::SHA1.new("#{key.hash}#{self.record_id}").to_s
|
189
|
+
@subtrees.each do |tree_id, tree|
|
190
|
+
return tree if tree_id > key_id
|
191
|
+
end
|
192
|
+
return @subtrees.values.first
|
193
|
+
end
|
194
|
+
|
195
|
+
#
|
196
|
+
# Split this Tree by creating @subtrees,
|
197
|
+
# then putting all @elements in them and then emptying @elements.
|
198
|
+
#
|
199
|
+
def split!
|
200
|
+
raise "Cant split twice!" unless @elements
|
201
|
+
|
202
|
+
@subtrees = RBTree.new
|
203
|
+
@subtrees.extend(Archipelago::Current::ThreadedCollection)
|
204
|
+
step = (1 << 160) / @width
|
205
|
+
0.upto(@width - 1) do |n|
|
206
|
+
@subtrees["%x" % (n * step)] = Tree.get_instance(:width => @width)
|
207
|
+
end
|
208
|
+
@elements.each do |key, value|
|
209
|
+
subtree_for(key)[key] = value
|
210
|
+
end
|
211
|
+
@elements = nil
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|