torquebox-cache 2.0.0.beta1-java
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/active_support/cache/torque_box_store.rb +124 -0
- data/lib/cache.rb +420 -0
- data/lib/cache_listener.rb +46 -0
- data/lib/datamapper/dm-infinispan-adapter.rb +128 -0
- data/lib/datamapper/model.rb +185 -0
- data/lib/datamapper/search.rb +145 -0
- data/lib/dm-infinispan-adapter.rb +1 -0
- data/lib/gem_hook.rb +22 -0
- data/lib/torquebox-cache.jar +0 -0
- data/lib/torquebox-cache.rb +13 -0
- data/licenses/lgpl-2.1.txt +502 -0
- data/spec/cache_listener_spec.rb +93 -0
- data/spec/cache_spec.rb +361 -0
- data/spec/dm-infinispan-adapter_spec.rb +375 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/torque_box_store_spec.rb +220 -0
- metadata +161 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright 2008-2011 Red Hat, Inc, and individual contributors.
|
2
|
+
#
|
3
|
+
# This is free software; you can redistribute it and/or modify it
|
4
|
+
# under the terms of the GNU Lesser General Public License as
|
5
|
+
# published by the Free Software Foundation; either version 2.1 of
|
6
|
+
# the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This software is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this software; if not, write to the Free
|
15
|
+
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
16
|
+
# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
17
|
+
|
18
|
+
module TorqueBox
|
19
|
+
module Infinispan
|
20
|
+
class CacheListener
|
21
|
+
|
22
|
+
def event_fired( event )
|
23
|
+
event_type = event.get_type.to_s.downcase
|
24
|
+
if respond_to? event_type
|
25
|
+
self.send( event_type, event )
|
26
|
+
else
|
27
|
+
puts "#{self.class.name}##{event_type} not implemented."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
add_class_annotation( { org.infinispan.notifications.Listener => { } } )
|
32
|
+
add_method_signature( "event_fired", [java.lang.Void::TYPE, org.infinispan.notifications.cachelistener.event.Event] )
|
33
|
+
add_method_annotation( "event_fired",
|
34
|
+
{ org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated => {},
|
35
|
+
org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved => {},
|
36
|
+
org.infinispan.notifications.cachelistener.annotation.CacheEntryModified => {},
|
37
|
+
org.infinispan.notifications.cachelistener.annotation.CacheEntryEvicted => {},
|
38
|
+
org.infinispan.notifications.cachelistener.annotation.CacheEntryActivated => {},
|
39
|
+
org.infinispan.notifications.cachelistener.annotation.CacheEntryEvicted => {},
|
40
|
+
org.infinispan.notifications.cachelistener.annotation.CacheEntryVisited => {}})
|
41
|
+
become_java!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2011 Red Hat, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
require "digest/sha1"
|
17
|
+
require 'dm-core'
|
18
|
+
require 'cache'
|
19
|
+
require 'json'
|
20
|
+
require 'torquebox-cache' # is this needed?
|
21
|
+
require 'datamapper/model'
|
22
|
+
require 'datamapper/search'
|
23
|
+
|
24
|
+
|
25
|
+
module DataMapper::Adapters
|
26
|
+
|
27
|
+
class InfinispanAdapter < AbstractAdapter
|
28
|
+
|
29
|
+
include TorqueBox::Infinispan
|
30
|
+
|
31
|
+
DataMapper::Model.append_inclusions( Infinispan::Model )
|
32
|
+
|
33
|
+
def initialize( name, options )
|
34
|
+
super
|
35
|
+
@options = options.dup
|
36
|
+
@metadata = @options.dup
|
37
|
+
@options[:name] = name.to_s
|
38
|
+
@options[:index] = true
|
39
|
+
@metadata[:name] = name.to_s + "/metadata"
|
40
|
+
@cache = Cache.new( @options )
|
41
|
+
@metadata_cache = Cache.new( @metadata )
|
42
|
+
@search = Infinispan::Search.new(cache, lambda{ |v| self.deserialize(v) })
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def create( resources )
|
47
|
+
cache.transaction do
|
48
|
+
resources.each do |resource|
|
49
|
+
initialize_serial( resource, @metadata_cache.increment( index_for( resource ) ) )
|
50
|
+
cache.put( key( resource ), serialize( resource ) )
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def read( query )
|
56
|
+
records = []
|
57
|
+
query.filter_records(@search.search( query ))
|
58
|
+
end
|
59
|
+
|
60
|
+
def update( attributes, collection )
|
61
|
+
attributes = attributes_as_fields(attributes)
|
62
|
+
cache.transaction do
|
63
|
+
collection.each do |resource|
|
64
|
+
resource.attributes(:field).merge(attributes)
|
65
|
+
cache.put( key(resource), serialize(resource) )
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete( collection )
|
71
|
+
cache.transaction do
|
72
|
+
collection.each do |resource|
|
73
|
+
cache.remove( key(resource) )
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop
|
79
|
+
cache.stop
|
80
|
+
end
|
81
|
+
|
82
|
+
def serialize(resource)
|
83
|
+
resource.is_a?(DataMapper::Resource) ? resource : resource.to_json
|
84
|
+
end
|
85
|
+
|
86
|
+
def deserialize(value)
|
87
|
+
value.is_a?(String) ? JSON.parse(value) : value
|
88
|
+
end
|
89
|
+
|
90
|
+
def search_manager
|
91
|
+
@search.search_manager
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def cache
|
96
|
+
@cache
|
97
|
+
end
|
98
|
+
|
99
|
+
def metadata_cache
|
100
|
+
@metadata_cache
|
101
|
+
end
|
102
|
+
|
103
|
+
def next_id(resource)
|
104
|
+
Digest::SHA1.hexdigest(Time.now.to_i + rand(1000000000).to_s)[1..length].to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
def key( resource )
|
108
|
+
model = resource.model
|
109
|
+
key = resource.key.nil? ? '' : resource.key.join('/')
|
110
|
+
"#{model}/#{key}/#{resource.id}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def index_for( resource )
|
114
|
+
resource.model.name + ".index"
|
115
|
+
end
|
116
|
+
|
117
|
+
def all_records
|
118
|
+
records = []
|
119
|
+
cache.keys.each do |key|
|
120
|
+
value = cache.get(key)
|
121
|
+
records << deserialize(value) if value
|
122
|
+
end
|
123
|
+
records
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
@@ -0,0 +1,185 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2011 Red Hat, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
require 'dm-serializer'
|
17
|
+
require 'jruby/core_ext'
|
18
|
+
require 'json'
|
19
|
+
|
20
|
+
|
21
|
+
module Infinispan
|
22
|
+
JVoid = java.lang.Void::TYPE
|
23
|
+
|
24
|
+
module Model
|
25
|
+
|
26
|
+
# TODO enhance TYPEs list
|
27
|
+
TYPES = {
|
28
|
+
::String => java.lang.String,
|
29
|
+
::Integer => java.lang.Integer,
|
30
|
+
::Float => java.lang.Double,
|
31
|
+
::BigDecimal => java.math.BigDecimal,
|
32
|
+
::Date => java.util.Date,
|
33
|
+
::DateTime => java.util.Date,
|
34
|
+
::Time => java.util.Date,
|
35
|
+
::TrueClass => java.lang.Boolean
|
36
|
+
}
|
37
|
+
|
38
|
+
def self.included(model)
|
39
|
+
model.extend(ClassMethods)
|
40
|
+
include java.io.Serializable
|
41
|
+
|
42
|
+
unless model.mapped? model.name
|
43
|
+
[:auto_migrate!, :auto_upgrade!, :create, :all, :copy, :first, :first_or_create, :first_or_new, :get, :last, :load].each do |method|
|
44
|
+
model.before_class_method(method, :configure_index)
|
45
|
+
end
|
46
|
+
|
47
|
+
[:save, :update, :destroy, :update_attributes].each do |method|
|
48
|
+
model.before(method) { model.configure_index }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def deserialize_to
|
54
|
+
self.class.name
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_a_with_hack?( thing )
|
58
|
+
thing == java.lang.Object || is_a_without_hack?( thing )
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :is_a_without_hack?, :is_a?
|
62
|
+
alias_method :is_a?, :is_a_with_hack?
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
|
66
|
+
@@mapped = {}
|
67
|
+
|
68
|
+
def auto_upgrade!
|
69
|
+
configure_index
|
70
|
+
end
|
71
|
+
|
72
|
+
def auto_migrate!
|
73
|
+
destroy
|
74
|
+
configure_index
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_java_type(type)
|
78
|
+
TYPES[type] || self.to_java_type(type.primitive)
|
79
|
+
end
|
80
|
+
|
81
|
+
def mapped?( type )
|
82
|
+
@@mapped[type]
|
83
|
+
end
|
84
|
+
|
85
|
+
def configure_index
|
86
|
+
unless mapped?( name )
|
87
|
+
configure_index!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def configure_index!
|
92
|
+
TorqueBox::Infinispan::Cache.log( "Configuring dm-infinispan-adapter model #{name}" )
|
93
|
+
properties().each do |prop|
|
94
|
+
TorqueBox::Infinispan::Cache.log( "Adding property #{prop.inspect}" )
|
95
|
+
add_java_property(prop)
|
96
|
+
TorqueBox::Infinispan::Cache.log( "Added property #{prop.inspect}" )
|
97
|
+
end
|
98
|
+
|
99
|
+
annotation = {
|
100
|
+
org.hibernate.search.annotations.Indexed => {},
|
101
|
+
org.hibernate.search.annotations.ProvidedId => {},
|
102
|
+
org.infinispan.marshall.SerializeWith => { "value" => org.torquebox.cache.marshalling.JsonExternalizer.java_class }
|
103
|
+
}
|
104
|
+
|
105
|
+
add_class_annotation( annotation )
|
106
|
+
|
107
|
+
# Wonder twin powers... ACTIVATE!
|
108
|
+
java_class = become_java!(false)
|
109
|
+
|
110
|
+
@@mapped[name] = true
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_java_property(prop)
|
114
|
+
name = prop.name
|
115
|
+
type = prop.class
|
116
|
+
|
117
|
+
column_name = prop.field
|
118
|
+
annotation = {}
|
119
|
+
|
120
|
+
annotation[org.hibernate.search.annotations.Field] = {}
|
121
|
+
|
122
|
+
get_name = "get#{name.to_s.capitalize}"
|
123
|
+
set_name = "set#{name.to_s.capitalize}"
|
124
|
+
|
125
|
+
# TODO Time, Discriminator, EmbededValue
|
126
|
+
# to consider: in mu opinion those methods should set from/get to java objects...
|
127
|
+
if (type == DataMapper::Property::Date)
|
128
|
+
class_eval <<-EOT
|
129
|
+
def #{set_name.intern} (d)
|
130
|
+
attribute_set(:#{name} , d.nil? ? nil : Date.civil(d.year + 1900, d.month + 1, d.date))
|
131
|
+
end
|
132
|
+
EOT
|
133
|
+
class_eval <<-EOT
|
134
|
+
def #{get_name.intern}
|
135
|
+
d = attribute_get(:#{name} )
|
136
|
+
java.util.Date.new( (Time.mktime(d.year, d.month, d.day).to_i * 1000) ) if d
|
137
|
+
end
|
138
|
+
EOT
|
139
|
+
elsif (type == DataMapper::Property::DateTime)
|
140
|
+
class_eval <<-EOT
|
141
|
+
def #{set_name.intern} (d)
|
142
|
+
attribute_set(:#{name} , d.nil? ? nil : DateTime.civil(d.year + 1900, d.month + 1, d.date, d.hours, d.minutes, d.seconds))
|
143
|
+
end
|
144
|
+
EOT
|
145
|
+
class_eval <<-EOT
|
146
|
+
def #{get_name.intern}
|
147
|
+
d = attribute_get(:#{name} )
|
148
|
+
java.util.Date.new( (Time.mktime(d.year, d.month, d.day, d.hour, d.min, d.sec, 0).to_i * 1000) ) if d
|
149
|
+
end
|
150
|
+
EOT
|
151
|
+
elsif (type.to_s == BigDecimal || type == DataMapper::Property::Decimal)
|
152
|
+
class_eval <<-EOT
|
153
|
+
def #{set_name.intern} (d)
|
154
|
+
attribute_set(:#{name} , d.nil? ? nil :#{type}.new(d.to_s))
|
155
|
+
end
|
156
|
+
EOT
|
157
|
+
class_eval <<-EOT
|
158
|
+
def #{get_name.intern}
|
159
|
+
d = attribute_get(:#{name} )
|
160
|
+
java.math.BigDecimal.new(d.to_i) if d
|
161
|
+
end
|
162
|
+
EOT
|
163
|
+
else
|
164
|
+
class_eval <<-EOT
|
165
|
+
def #{set_name.intern} (d)
|
166
|
+
attribute_set(:#{name} , d)
|
167
|
+
end
|
168
|
+
EOT
|
169
|
+
class_eval <<-EOT
|
170
|
+
def #{get_name.intern}
|
171
|
+
d = attribute_get(:#{name} )
|
172
|
+
d
|
173
|
+
end
|
174
|
+
EOT
|
175
|
+
end
|
176
|
+
|
177
|
+
mapped_type = to_java_type(type)
|
178
|
+
add_method_signature get_name, [mapped_type]
|
179
|
+
add_method_annotation get_name, annotation
|
180
|
+
add_method_signature set_name, [JVoid, mapped_type]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2011 Red Hat, Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
module Infinispan
|
18
|
+
|
19
|
+
class Search
|
20
|
+
|
21
|
+
def initialize(cache, deserializer)
|
22
|
+
@cache = cache
|
23
|
+
@deserializer = deserializer
|
24
|
+
begin
|
25
|
+
@search_manager = cache.search_manager
|
26
|
+
rescue Exception => e
|
27
|
+
cache.log( "Infinispan SearchManager not available for cache: #{cache.name}", 'ERROR' )
|
28
|
+
cache.log( e.message, 'ERROR' )
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def search( query )
|
33
|
+
if @search_manager
|
34
|
+
cache_query = search_manager.get_query( build_query( query ), query.model.java_class )
|
35
|
+
cache_query.list.collect { |record| deserialize(record) }
|
36
|
+
else
|
37
|
+
cache.all.select do |r|
|
38
|
+
record = deserialize(r)
|
39
|
+
record.class == query.model
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def search_manager
|
45
|
+
@search_manager
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def build_query( query )
|
50
|
+
builder = search_manager.build_query_builder_for_class( query.model.java_class ).get
|
51
|
+
query = query.conditions.nil? ? builder.all.create_query : handle_condition( builder, query.conditions.first )
|
52
|
+
#puts "LUCENE QUERY: #{query.to_s}"
|
53
|
+
query
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_condition( builder, condition )
|
57
|
+
#puts "CONDITION: #{condition.inspect} <<<>>> #{condition}"
|
58
|
+
#puts "CONDITION CLASS: #{condition.class}"
|
59
|
+
#puts "CONDITION OPERANDS: #{condition.operands.inspect}" if condition.respond_to? :operands
|
60
|
+
#puts "CONDITION VALUE: #{condition.value}"
|
61
|
+
#puts "CONDITION SUBJECT: #{condition.subject.inspect}"
|
62
|
+
if condition.class == DataMapper::Query::Conditions::OrOperation
|
63
|
+
terms = condition.operands.each do |op|
|
64
|
+
builder.bool.should( handle_condition( builder, op ) )
|
65
|
+
end
|
66
|
+
builder.all.create_query
|
67
|
+
elsif condition.class == DataMapper::Query::Conditions::NotOperation
|
68
|
+
handle_not_operation( builder, condition )
|
69
|
+
elsif condition.class == DataMapper::Query::Conditions::EqualToComparison
|
70
|
+
handle_equal_to( builder, condition )
|
71
|
+
elsif condition.class == DataMapper::Query::Conditions::InclusionComparison
|
72
|
+
handle_inclusion( builder, condition )
|
73
|
+
elsif condition.class == DataMapper::Query::Conditions::RegexpComparison
|
74
|
+
handle_regex( builder, condition )
|
75
|
+
else
|
76
|
+
builder.all.create_query
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_regex( builder, condition )
|
81
|
+
field = condition.subject.name
|
82
|
+
# TODO Figure out how hibernate search/lucene deal with regexp
|
83
|
+
value = condition.value.nil? ? "?*" : "*" + condition.value.source.gsub('/','') + "*"
|
84
|
+
builder.keyword.wildcard.on_field(field).matching(value).create_query
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_inclusion( builder, condition )
|
88
|
+
#puts "RANGE: #{condition.value.class} #{condition.value}"
|
89
|
+
if condition.value.is_a? Range
|
90
|
+
# TODO: Deal with Time
|
91
|
+
if ((condition.subject.class == DataMapper::Property::DateTime) ||
|
92
|
+
(condition.subject.class == DataMapper::Property::Date))
|
93
|
+
rng = builder.range.on_field(condition.subject.name).from(convert_date(condition.value.begin)).to(convert_date(condition.value.end))
|
94
|
+
else
|
95
|
+
rng = builder.range.on_field(condition.subject.name).from(condition.value.begin).to(condition.value.end)
|
96
|
+
end
|
97
|
+
condition.value.exclude_end? ? rng.exclude_limit.create_query : rng.create_query
|
98
|
+
else # an Array
|
99
|
+
match = condition.value.collect { |v| v }.join(' ')
|
100
|
+
if match.empty?
|
101
|
+
# we should find nothing
|
102
|
+
builder.bool.must( builder.all.create_query ).not.create_query
|
103
|
+
else
|
104
|
+
builder.keyword.on_field( condition.subject.name ).matching( match ).create_query
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def convert_date(date)
|
110
|
+
java.util.Date.new(Time.mktime(date.year, date.month, date.day, date.hour, date.min, date.sec, 0).to_i*1000) if date
|
111
|
+
end
|
112
|
+
|
113
|
+
def handle_equal_to( builder, condition )
|
114
|
+
field = condition.subject.name
|
115
|
+
value = condition.value.nil? ? "?*" : condition.value.to_s
|
116
|
+
if !value.nil? && (value.include?( '?' ) || value.include?( '*' ))
|
117
|
+
builder.keyword.wildcard.on_field(field).matching(value).create_query
|
118
|
+
else
|
119
|
+
builder.keyword.on_field(field).matching(value).create_query
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def handle_not_operation( builder, operation )
|
124
|
+
condition = operation.operands.first
|
125
|
+
if (condition.class == DataMapper::Query::Conditions::EqualToComparison && condition.value.nil?)
|
126
|
+
# not nil means everything
|
127
|
+
everything = DataMapper::Query::Conditions::EqualToComparison.new( condition.subject, '*' )
|
128
|
+
handle_condition( builder, everything )
|
129
|
+
else
|
130
|
+
builder.bool.must( handle_condition( builder, condition ) ).not.create_query
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def cache
|
135
|
+
@cache
|
136
|
+
end
|
137
|
+
|
138
|
+
def deserialize(value)
|
139
|
+
@deserializer.call(value)
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
|