rtm-activerecord 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/DISCLAIMER +13 -0
  2. data/LICENSE +201 -0
  3. data/README +4 -0
  4. data/lib/rtm/activerecord/001_initial_schema.rb +119 -0
  5. data/lib/rtm/activerecord/association_and_role.rb +54 -0
  6. data/lib/rtm/activerecord/base.rb +83 -0
  7. data/lib/rtm/activerecord/connect.rb +106 -0
  8. data/lib/rtm/activerecord/io/from_xtm2.rb +266 -0
  9. data/lib/rtm/activerecord/io/from_xtm2_libxml.rb +97 -0
  10. data/lib/rtm/activerecord/io/to_jtm.rb +141 -0
  11. data/lib/rtm/activerecord/io/to_string.rb +130 -0
  12. data/lib/rtm/activerecord/io/to_xtm1.rb +162 -0
  13. data/lib/rtm/activerecord/io/to_xtm2.rb +163 -0
  14. data/lib/rtm/activerecord/io/to_yaml.rb +132 -0
  15. data/lib/rtm/activerecord/literal_index.rb +38 -0
  16. data/lib/rtm/activerecord/locators.rb +58 -0
  17. data/lib/rtm/activerecord/merging.rb +310 -0
  18. data/lib/rtm/activerecord/name_variant_occurrence.rb +86 -0
  19. data/lib/rtm/activerecord/persistent_code_output.rb +22 -0
  20. data/lib/rtm/activerecord/quaaxtm2rtm.rb +116 -0
  21. data/lib/rtm/activerecord/quaaxtm2rtmviews.rb +137 -0
  22. data/lib/rtm/activerecord/set_wrapper.rb +101 -0
  23. data/lib/rtm/activerecord/sugar/association/hash_access.rb +22 -0
  24. data/lib/rtm/activerecord/sugar/role/counterparts.rb +56 -0
  25. data/lib/rtm/activerecord/sugar/topic/characteristics.rb +15 -0
  26. data/lib/rtm/activerecord/sugar/topic/counterparts.rb +18 -0
  27. data/lib/rtm/activerecord/sugar/topic/hash_access.rb +78 -0
  28. data/lib/rtm/activerecord/sugar/topic/identifier_direct.rb +14 -0
  29. data/lib/rtm/activerecord/sugar/topic/predefined_associations.rb +53 -0
  30. data/lib/rtm/activerecord/tm_construct.rb +66 -0
  31. data/lib/rtm/activerecord/tm_delegator.rb +417 -0
  32. data/lib/rtm/activerecord/tm_set_delegator.rb +226 -0
  33. data/lib/rtm/activerecord/tmdm.rb +309 -0
  34. data/lib/rtm/activerecord/topic.rb +204 -0
  35. data/lib/rtm/activerecord/topic_map.rb +435 -0
  36. data/lib/rtm/activerecord/traverse_associations.rb +90 -0
  37. data/lib/rtm/activerecord.rb +106 -0
  38. metadata +120 -0
@@ -0,0 +1,226 @@
1
+ # Copyright: Copyright 2009 Topic Maps Lab, University of Leipzig.
2
+ # License: Apache License, Version 2.0
3
+
4
+ module RTM::AR
5
+ class TMSetDelegator < TMDelegator
6
+ include Enumerable
7
+ # attr_reader :content_class_name
8
+ def initialize(obj,parent,type)
9
+ @obj = obj
10
+ @parent = parent
11
+ @type = type
12
+ end
13
+
14
+ # This class method wraps a Set completely while the instance method wraps one single contained object
15
+ def self.wrap(obj, parent=nil, type=nil)
16
+ return nil unless obj
17
+ raise "Double wrapping" if obj.respond_to?(:__getobj__)
18
+ self.new(obj, parent, type)
19
+ end
20
+
21
+ def add(obj)
22
+ return unless obj
23
+
24
+ old = @obj.detect { |x| x == obj } # can't that be done easier?
25
+ if old
26
+ old.merge obj
27
+ else
28
+ if obj.respond_to? :__getobj__
29
+ @obj << obj.__getobj__
30
+ else
31
+ case @type
32
+ when :Topic
33
+ @obj << @parent.topic_map.get!(obj).__getobj__
34
+ @parent.__getobj__.reload
35
+
36
+ when :ItemIdentifier
37
+ if @parent.class.name.to_s =~ /::Topic$/
38
+ tbi = @parent.topic_map._item_identifier(obj)
39
+ if tbi && tmc=tbi.topic_map_construct
40
+ if tmc.class.name =~ /::Topic$/
41
+ return @parent if tmc == @parent.__getobj__
42
+ result = Topic.wrap(tmc).merge @parent
43
+ return result
44
+ else
45
+ raise "Duplicate Item Identifier"
46
+ end
47
+ end
48
+ tbsi = @parent.topic_map._subject_identifier(obj.to_s)
49
+ if tbsi
50
+ if t=tbsi.topic
51
+ return @parent if t == @parent.__getobj__
52
+ result = Topic.wrap(t).merge @parent
53
+ # after merging, we still need to add the II
54
+ result.__getobj__.item_identifiers.create(:reference => obj.to_s, :topic_map_id => @parent.topic_map.__getobj__.id)
55
+ return result
56
+ end
57
+ end
58
+ end
59
+ result = @obj << @parent.topic_map._item_identifier!(obj.to_s)
60
+ return result
61
+
62
+ when :SubjectIdentifier
63
+ # check for existing item identifier
64
+ puts
65
+ puts "trying to add SI to a topic"
66
+ tbi = @parent.topic_map._item_identifier(obj)
67
+ if tbi && tmc=tbi.construct
68
+ puts "something with this II exists"
69
+ if tmc.class.name =~ /::Topic$/
70
+ puts "it exists as a topic"
71
+ puts tmc == @parent.__getobj__
72
+ return @parent if tmc == @parent.__getobj__
73
+
74
+ puts "trying to merge"
75
+ puts "BEFORE"
76
+ begin
77
+ result = Topic.wrap(tmc).merge(@parent)
78
+ rescue Exception => e
79
+ puts e.inspect
80
+ raise e
81
+ end
82
+ puts "AFTER"
83
+ puts "done merging, now adding SI"
84
+
85
+ #result = @parent.merge(Topic.wrap(tmc))
86
+ # after merging, we still need to add the SI
87
+ result.__getobj__.subject_identifiers.create(:reference => obj.to_s, :topic_map_id => @parent.topic_map.__getobj__.id)
88
+ puts "done adding SI, now reloading"
89
+ result.reload
90
+ puts "done reloading"
91
+ return result
92
+ else
93
+ warn("This subject identifier IRI already belongs to another topic map construct (not a topic)")
94
+ end
95
+ end
96
+ puts "checking for existing SI"
97
+ # check for existing subject identifier
98
+ tbsi = @parent.topic_map._subject_identifier(obj.to_s)
99
+ if tbsi
100
+ puts "SI exists"
101
+ if true && t=tbsi.topic #the single = is intentional, the "true &&" just makes netbeans not raise a warning
102
+ return @parent if t == @parent.__getobj__
103
+ result = Topic.wrap(t).merge @parent
104
+ return result
105
+ end
106
+ end
107
+ @obj.create(:reference => obj.to_s, :topic_map_id => @parent.topic_map.__getobj__.id)
108
+
109
+ when :SubjectLocator
110
+ tbsl = @parent.topic_map._subject_locator(obj.to_s)
111
+ if tbsl
112
+ if true && t=tbsl.topic #the single = is intentional, the "true &&" just makes netbeans not raise a warning
113
+ return @parent if t == @parent.__getobj__
114
+ result = Topic.wrap(t).merge @parent
115
+ return result
116
+ end
117
+ end
118
+ @obj.create(:reference => obj.to_s, :topic_map_id => @parent.topic_map.__getobj__.id)
119
+
120
+ end
121
+ end
122
+ end
123
+ end
124
+ alias :<< :add
125
+
126
+ def add_all(objs)
127
+ return unless objs
128
+ objs.each {|obj| add(obj)}
129
+ true
130
+ end
131
+
132
+ def each(&b)
133
+ @obj.each { |e| yield wrap(e)}
134
+ end
135
+
136
+ def size
137
+ @obj.size
138
+ end
139
+ alias :length :size
140
+
141
+ def empty?
142
+ @obj.empty?
143
+ end
144
+
145
+ def delete(obj)
146
+ obj = obj.__getobj__ if obj.respond_to? :__getobj__
147
+ case @type
148
+ when :ItemIdentifier, :SubjectIdentifier, :SubjectLocator
149
+ obj = @obj.find_by_reference(@parent.topic_map.resolve(obj.to_s)) if obj.is_a? String
150
+ when :Topic
151
+ obj = @parent.topic_map.get!(obj).__getobj__
152
+ end
153
+ @obj.delete(obj)
154
+ # item_identifiers: remove also from topicMap
155
+ #removed_event obj if respond_to? :removed_event
156
+ end
157
+ alias :remove :delete
158
+
159
+ def include?(obj)
160
+ return @obj.include?(obj.__getobj__)
161
+ #@obj.each { |e| return true if e == obj } # T#ODO support for get
162
+ #false
163
+ end
164
+
165
+ def first
166
+ wrap(@obj.entries.first)
167
+ end
168
+ def last
169
+ wrap(@obj.entries.last)
170
+ end
171
+
172
+ def to_s
173
+ "[#{@obj.entries.map { |e| wrap(e).to_s }.join(", ") }]"
174
+ end
175
+ def [](i)
176
+ wrap(@obj[i])
177
+ end
178
+
179
+ def content_class
180
+ # @content_class ||= RTM.const_get(@content_class_name)
181
+ @content_class ||= RTM.const_get("#{@content_class_name}MemImpl")
182
+ end
183
+
184
+ def find(*args)
185
+ res = @obj.find(*args)
186
+ if res.respond_to? :each
187
+ Constructs.wrap(res)
188
+ else
189
+ Construct.wrap(res)
190
+ end
191
+ end
192
+
193
+ def &(other)
194
+ @obj.to_a & other.to_a
195
+ end
196
+
197
+ alias :old_method_missing :method_missing
198
+ def method_missing(method_name, *args)
199
+ if @obj.size > 0 && first.respond_to?(method_name) && (![:__getobj__, :__setobj__].include?(method_name))
200
+ a = []
201
+ inject(a) {|all,single| all << single.send(method_name, *args)}
202
+ a
203
+ else
204
+ old_method_missing(method_name, *args)
205
+ end
206
+ end
207
+
208
+ alias :old_respond_to? :respond_to?
209
+ def respond_to?(method_name)
210
+ resp = old_respond_to?(method_name)
211
+ return resp if resp # i.e. if true
212
+ return false if [:__getobj__, :__setobj__].include?(method_name)
213
+ # ... and ask first child otherwise
214
+ @obj.size > 0 && first.respond_to?(method_name)
215
+ end
216
+
217
+ def ==(x)
218
+ self.to_a == x.to_a
219
+ end
220
+
221
+ # TMSetDelegator#to_a doesn't help as thought, but maybe we come back to that l8r...
222
+ #def to_a
223
+ # @obj.map {|o| wrap(o)}
224
+ #end
225
+ end
226
+ end
@@ -0,0 +1,309 @@
1
+ # Copyright: Copyright 2009 Topic Maps Lab, University of Leipzig.
2
+ # License: Apache License, Version 2.0
3
+
4
+ old_verbose=$VERBOSE
5
+ $VERBOSE=false
6
+ require 'active_record'
7
+ $VERBOSE=old_verbose
8
+
9
+ module RTM::AR
10
+ # The Active Record TMDM backend
11
+ module TMDM
12
+ # Moves all fields in a collection to another owner (for classes using belongs_to in AR::Base class)
13
+ module Movable
14
+ # Takes a list of [field,from, to] arguments.
15
+ # RTM::AR::TMDM::ItemIdentifier.move_all(
16
+ # ["construct_id", other.__getobj__.id, self.__getobj__.id],
17
+ # ["construct_type", other.__getobj__.class.name, self.__getobj__.class.name]
18
+ # )
19
+ # or
20
+ # RTM::AR::TMDM::Variant.move_all(["name_id", other.__getobj__.id, self.__getobj__.id])
21
+
22
+ #
23
+ def move_all(*args)
24
+ raise "All parameters must have a size of 3: [field_name, from, to]" unless args.all?{|a| a.size == 3}
25
+ set_part = ""
26
+ where_part = ""
27
+ args.each do |field, from, to|
28
+ set_part += ", " unless set_part.empty?
29
+ where_part += " and " unless where_part.empty?
30
+ from = "'#{from}'" unless from.is_a? Numeric
31
+ to = "'#{to}'" unless to.is_a? Numeric
32
+ where_part += "#{field} = #{from}"
33
+ set_part += "#{field} = #{to}"
34
+ end
35
+ #puts "setpart: #{set_part.inspect}\nwherepart: #{where_part.inspect}"
36
+ res = self.update_all(set_part, where_part)
37
+ #puts "result: #{res.inspect}"
38
+ end
39
+ end
40
+
41
+ class Construct < ActiveRecord::Base
42
+ self.store_full_sti_class = false
43
+ extend Movable
44
+ class << self
45
+ def abstract_class?
46
+ self == Construct
47
+ end
48
+ def alias_ar_relation(new,old)
49
+ alias_method new, old
50
+ alias_method "#{new}=", "#{old}="
51
+ end
52
+ alias :alias_belongs_to :alias_ar_relation
53
+ alias :alias_has_many :alias_ar_relation
54
+ def belongs_to_with_alias(name, synonym)
55
+ belongs_to name
56
+ alias_belongs_to synonym, name
57
+ end
58
+ def has_many_with_alias(name, synonym, *args)
59
+ has_many name, *args
60
+ alias_has_many synonym, name
61
+ end
62
+ def belongs_to_parent(parent)
63
+ belongs_to parent
64
+ alias_belongs_to :parent, parent
65
+ end
66
+ def this_is_a_typed_object
67
+ belongs_to :ttype, :class_name => "Topic", :foreign_key => :ttype_id
68
+ module_eval(<<-EOS, "(__RTM_AR_TMDM_TYPED_OBJECT__)", 1)
69
+ def type=(ptype)
70
+ self.ttype=ptype
71
+ end
72
+ def type
73
+ self.ttype
74
+ end
75
+ EOS
76
+ end
77
+ end
78
+ has_many :item_identifiers, :as => :construct, :dependent => :destroy
79
+ def topic_map
80
+ parent.topic_map
81
+ end
82
+ def move_to(new_parent)
83
+ self.parent = new_parent
84
+ end
85
+
86
+ end
87
+ class Reifiable < Construct
88
+ def self.abstract_class?
89
+ self == Reifiable
90
+ end
91
+ has_one :reifier, :class_name => "Topic", :as => :reified
92
+ end
93
+ class ScopedObject < Reifiable
94
+ def self.abstract_class?
95
+ self == ScopedObject
96
+ end
97
+ has_many :scoped_objects_topics, :as => :scoped_object, :dependent => :destroy
98
+ has_many :scope, :through => :scoped_objects_topics, :source => :topic
99
+ # def scope
100
+ # self.scoped_objects_topics.map { |sot| sot.topic }
101
+ # end
102
+ # def add_scoping_topic(topic) # this is not ruby-ish but at least it works, would need a complete association proxy instead...
103
+ # sot = ScopedObjectsTopic.create :topic => topic, :scoped_object => self
104
+ # self
105
+ # # sot = ScopedObjectsTopic.new :topic => topic, :scoped_object => self
106
+ # # self.scoped_objects_topics << sot
107
+ # # self
108
+ # end
109
+ # def remove_scoping_topic(topic) # this is not ruby-ish but at least it works, would need a complete association proxy instead...
110
+ # self.scoped_objects_topics.select { |sot| sot.topic == topic }.destroy
111
+ # self
112
+ # end
113
+ end
114
+ class ScopedObjectsTopic < ActiveRecord::Base
115
+ self.store_full_sti_class = false
116
+ extend Movable
117
+ belongs_to :topic, :class_name => 'RTM::AR::TMDM::Topic'
118
+ belongs_to :scoped_object, :polymorphic => true
119
+ belongs_to :association, :class_name => 'RTM::AR::TMDM::Association', :foreign_key => "scoped_object_id"
120
+ belongs_to :name, :class_name => 'RTM::AR::TMDM::Name', :foreign_key => "scoped_object_id"
121
+ belongs_to :variant, :class_name => 'RTM::AR::TMDM::Variant', :foreign_key => "scoped_object_id"
122
+ belongs_to :occurrence, :class_name => 'RTM::AR::TMDM::Occurrence', :foreign_key => "scoped_object_id"
123
+ end
124
+
125
+ class Locator < ActiveRecord::Base
126
+ self.store_full_sti_class = false
127
+ extend Movable
128
+ class << self
129
+ def abstract_class?
130
+ self == Locator
131
+ end
132
+ end
133
+ belongs_to :topic_map
134
+ end
135
+
136
+ class ItemIdentifier < Locator
137
+ belongs_to :construct, :polymorphic => true
138
+ def to_s
139
+ self.reference
140
+ end
141
+ def move_to(new_tmc)
142
+ #puts "\n\nmoving\n\t#{self.inspect}\n\nto\n\t#{new_tmc.inspect}"
143
+ #self.update_all "construct_id = #{new_tmc.id}, construct_type = '#{new_tmc.class.name}'",
144
+ #puts "changing id"
145
+ #puts self.construct_id = new_tmc.id
146
+ #puts "changing type"
147
+ #puts self.construct_type = new_tmc.class.name
148
+ self.construct = new_tmc
149
+ #puts "saving"
150
+ self.save
151
+ #
152
+ # das geht!!!
153
+ #self.class.update( self.id, "construct_id" => new_tmc.id)
154
+ end
155
+ end
156
+ class SubjectLocator < Locator
157
+ belongs_to :topic
158
+ def to_s
159
+ self.reference
160
+ end
161
+ end
162
+ class SubjectIdentifier < Locator
163
+ belongs_to :topic
164
+ def to_s
165
+ self.reference
166
+ end
167
+ end
168
+ # http://www.isotopicmaps.org/sam/sam-model/#d0e736
169
+ class Topic < Construct
170
+ belongs_to :reified, :polymorphic => true
171
+ belongs_to_parent :topic_map
172
+ has_many :subject_locators, :dependent => :destroy
173
+ has_many :subject_identifiers, :dependent => :destroy
174
+ has_many :occurrences, :dependent => :destroy
175
+ has_many_with_alias :names, :topic_names, :dependent => :destroy
176
+ has_many_with_alias :roles, :association_roles, :include => :association
177
+ has_many :associations, :through => :roles, :dependent => :destroy
178
+ #has_many :played_types, :through => :association_roles, :source => :type
179
+
180
+ has_many :typed_associations, :class_name => "Association", :foreign_key => :ttype_id
181
+ has_many :typed_roles, :class_name => "Role", :foreign_key => :ttype_id
182
+ has_many :typed_names, :class_name => "Name", :foreign_key => :ttype_id
183
+ has_many :typed_occurrences, :class_name => "Occurrence", :foreign_key => :ttype_id
184
+
185
+ has_many :scoped_objects_topics
186
+ has_many :scoped_associations, :through => :scoped_objects_topics, :source => :association,
187
+ :conditions => "scoped_objects_topics.scoped_object_type = 'RTM::AR::TMDM::Association'"
188
+ has_many :scoped_names, :through => :scoped_objects_topics, :source => :name,
189
+ :conditions => "scoped_objects_topics.scoped_object_type = 'RTM::AR::TMDM::Name'"
190
+ has_many :scoped_variants, :through => :scoped_objects_topics, :source => :variant,
191
+ :conditions => "scoped_objects_topics.scoped_object_type = 'RTM::AR::TMDM::Variant'"
192
+ has_many :scoped_occurrences, :through => :scoped_objects_topics, :source => :occurrence,
193
+ :conditions => "scoped_objects_topics.scoped_object_type = 'RTM::AR::TMDM::Occurrence'"
194
+
195
+ # def scoped_objects
196
+ # self.scoped_objects_topics.collect { |a| a.scoped_object } # temporary hack polymorphic has_many_through
197
+ # end
198
+ def scoped
199
+ scoped_associations + scoped_names + scoped_variants + scoped_occurrences
200
+ end
201
+
202
+ def typed
203
+ typed_associations + typed_roles + typed_names + typed_occurrences
204
+ end
205
+
206
+ end
207
+
208
+ class Role < Reifiable
209
+ belongs_to_parent :association
210
+ belongs_to_with_alias :topic, :player
211
+ this_is_a_typed_object
212
+ end
213
+
214
+ class Name < ScopedObject
215
+ belongs_to_parent :topic
216
+ this_is_a_typed_object
217
+ has_many :variants, :dependent => :destroy
218
+ end
219
+
220
+ class Association < ScopedObject
221
+ belongs_to_parent :topic_map
222
+ this_is_a_typed_object
223
+ has_many_with_alias :roles, :roles, :dependent => :destroy
224
+ def create_role(rplayer, ttype)
225
+ Role.create :parent => self, :player => rplayer, :ttype => ttype
226
+ end
227
+ alias :create_association_role :create_role
228
+ has_many :players, :through => :roles, :source => :topic
229
+ has_many :types, :through => :roles, :source => :ttype
230
+ end
231
+
232
+ class AssociationCache < Construct
233
+ set_table_name "associations_cache"
234
+ belongs_to_parent :topic_map
235
+ this_is_a_typed_object
236
+ has_many_with_alias :roles, :association_roles
237
+ belongs_to :role1, :class_name => "RTM::AR::TMDM::Role", :foreign_key => "role1_id"
238
+ belongs_to :player1, :class_name => "RTM::AR::TMDM::Topic", :foreign_key => "player1_id"
239
+ belongs_to :type1, :class_name => "RTM::AR::TMDM::Topic", :foreign_key => "type1_id"
240
+ belongs_to :role2, :class_name => "RTM::AR::TMDM::Role", :foreign_key => "role2_id"
241
+ belongs_to :player2, :class_name => "RTM::AR::TMDM::Topic", :foreign_key => "player2_id"
242
+ belongs_to :type2, :class_name => "RTM::AR::TMDM::Topic", :foreign_key => "type2_id"
243
+ # rolecount -> rcnt
244
+ end
245
+
246
+ class Occurrence < ScopedObject
247
+ belongs_to_parent :topic
248
+ this_is_a_typed_object
249
+ end
250
+
251
+ class Variant < ScopedObject
252
+ belongs_to_parent :name
253
+ def topic_map
254
+ parent.topic_map
255
+ end
256
+ end
257
+
258
+ class TopicMap < Reifiable
259
+ # real properties
260
+ has_many :topics, :dependent => :destroy, :include => [:subject_identifiers, :subject_locators]
261
+ has_many :associations, :dependent => :destroy, :include => :roles
262
+
263
+ # access to subproperties
264
+ has_many :locators, :class_name => "ItemIdentifier", :dependent => :destroy
265
+ has_many :subject_locators, :dependent => :destroy
266
+ has_many :subject_identifiers, :dependent => :destroy
267
+
268
+ has_many :names, :through => :topics
269
+ alias :topic_names :names
270
+
271
+ has_many :occurrences, :through => :topics
272
+ has_many :roles, :through => :associations
273
+ alias :association_roles :roles
274
+
275
+ %w[association role name occurrence].each do |m|
276
+ module_eval(<<-EOS, "(__AR_DELEGATOR_INDEX_PROPERTY_SET__)", 1)
277
+ def #{m}_types
278
+ topics.select{|t| t.#{m}s_typed.size > 0}
279
+ end
280
+ EOS
281
+ end
282
+
283
+ #typing
284
+ # has_many :association_types, :class_name => "Association", :foreign_key => :ttype_id
285
+ # has_many :role_types, :class_name => "Role", :foreign_key => :ttype_id
286
+ # has_many :name_types, :class_name => "Name", :foreign_key => :ttype_id
287
+ # has_many :occurrence_types, :class_name => "Occurrence", :foreign_key => :ttype_id
288
+
289
+ # has_many :association_types, :aka => :at, :type => :Topic, :wrap => true
290
+ # has_many :role_types, :aka => [:association_role_types,:art,:rt], :type => :Topic, :wrap => true
291
+ # has_many :name_types, :aka => [:name_types,:tnt,:nt], :type => :Topic, :wrap => true
292
+ # has_many :occurrence_types, :aka => :ot, :type => :Topic, :wrap => true
293
+
294
+
295
+ def topic_map
296
+ self
297
+ end
298
+
299
+ # # Creates a new Locator in this TopicMap
300
+ # def create_locator(reference, notation=nil)
301
+ # if notation
302
+ # ItemIdentifier.create :reference => reference, :notation => notation
303
+ # else
304
+ # ItemIdentifier.create :reference => reference # is there a better way to preserve the database default notation if none given?
305
+ # end
306
+ # end
307
+ end
308
+ end
309
+ end