rsence 2.0.0.6.pre → 2.0.0.7.pre
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/INSTALL.rdoc +39 -25
- data/VERSION +1 -1
- data/conf/default_conf.yaml +0 -3
- data/lib/conf/default.rb +59 -60
- data/lib/daemon/daemon.rb +269 -280
- data/lib/http/broker.rb +47 -14
- data/lib/http/rackup.rb +4 -0
- data/lib/http/request.rb +47 -49
- data/lib/http/response.rb +47 -45
- data/lib/plugins/gui_plugin.rb +22 -21
- data/lib/plugins/guiparser.rb +95 -94
- data/lib/plugins/plugin.rb +300 -295
- data/lib/plugins/plugin_plugins.rb +64 -40
- data/lib/plugins/plugin_sqlite_db.rb +63 -63
- data/lib/plugins/plugin_util.rb +95 -104
- data/lib/plugins/pluginmanager.rb +373 -414
- data/lib/plugins/plugins.rb +11 -4
- data/lib/plugins/servlet.rb +10 -9
- data/lib/session/msg.rb +249 -248
- data/lib/session/sessionmanager.rb +364 -373
- data/lib/session/sessionstorage.rb +265 -272
- data/lib/transporter/transporter.rb +164 -169
- data/lib/util/gzstring.rb +2 -0
- data/lib/values/hvalue.rb +224 -224
- data/lib/values/valuemanager.rb +98 -98
- data/plugins/index_html/index_html.rb +1 -32
- metadata +4 -4
data/lib/http/broker.rb
CHANGED
@@ -21,14 +21,11 @@ require 'http/response'
|
|
21
21
|
## Minimally WEBrick -compatible request object
|
22
22
|
require 'http/request'
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
Broker routes requests to the proper request processing instance
|
27
|
-
|
28
|
-
=end
|
29
|
-
|
24
|
+
# Broker routes requests to the proper request processing instance.
|
25
|
+
# It's the top-level http handler.
|
30
26
|
class Broker
|
31
27
|
|
28
|
+
# This method is called from Rack. The env is the Rack environment.
|
32
29
|
def call(env)
|
33
30
|
sleep @@ping_sim if @@ping_sim
|
34
31
|
unless @@transporter.online?
|
@@ -45,14 +42,18 @@ class Broker
|
|
45
42
|
response = Response.new
|
46
43
|
request_method = request.request_method.downcase
|
47
44
|
dispatcher = dispatcher_class.new( request, response )
|
48
|
-
dispatcher.send(request_method)
|
45
|
+
dispatcher.send( request_method )
|
49
46
|
content_type = dispatcher.content_type
|
50
|
-
# puts "encoding: #{response.body.encoding.inspect}"
|
51
47
|
response.header['Content-Length'] = response.body.length.to_s unless response.header.has_key?('Content-Length')
|
52
|
-
# puts [response.status, response.header, response.body].inspect
|
53
48
|
return [response.status, response.header, response.body]
|
54
49
|
end
|
55
50
|
|
51
|
+
# Returns a dynamically created "REST Dispatcher" kind of class that has
|
52
|
+
# request and response as instance variables and the rest of the Broker
|
53
|
+
# class as the superclass.
|
54
|
+
# It calls the method according to the http method.
|
55
|
+
# Called from #call
|
56
|
+
# Broker currently implements only get and post methods.
|
56
57
|
def dispatcher_class
|
57
58
|
@dispatcher ||= Class.new(self.class) do
|
58
59
|
attr_accessor :content_type
|
@@ -63,13 +64,23 @@ class Broker
|
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
66
|
-
|
67
|
+
# This method is used to create the Rack instance
|
68
|
+
# and set up itself accordingly.
|
69
|
+
# The transporter parameter is an instance of the Transporter class,
|
70
|
+
# which does all the actual delegation.
|
71
|
+
# The conf parameter contains a hash with at least the following:
|
72
|
+
# :bind_address, :port, :rack_require
|
73
|
+
def self.start( transporter, conf )
|
74
|
+
|
75
|
+
host = conf[:bind_address]
|
76
|
+
port = conf[:port]
|
77
|
+
|
67
78
|
@@transporter = transporter
|
68
|
-
|
69
|
-
if
|
79
|
+
latency = ::RSence.config[:http_server][:latency]
|
80
|
+
if latency == 0
|
70
81
|
@@ping_sim = false
|
71
82
|
else
|
72
|
-
@@ping_sim =
|
83
|
+
@@ping_sim = latency/1000.0
|
73
84
|
end
|
74
85
|
Thread.new do
|
75
86
|
Thread.pass
|
@@ -80,13 +91,34 @@ class Broker
|
|
80
91
|
end
|
81
92
|
@@transporter.online = true
|
82
93
|
end
|
94
|
+
|
95
|
+
require 'rack'
|
96
|
+
|
97
|
+
# Loads the selected web-server (default is 'mongrel')
|
98
|
+
rack_require = conf[:rack_require]
|
99
|
+
puts conf.inspect
|
100
|
+
puts "rack require: #{rack_require.inspect}" if RSence.args[:debug]
|
101
|
+
require rack_require
|
102
|
+
|
103
|
+
# Selects the handler for Rack
|
104
|
+
handler = {
|
105
|
+
'webrick' => lambda { Rack::Handler::WEBrick },
|
106
|
+
'ebb' => lambda { Rack::Handler::Ebb },
|
107
|
+
'thin' => lambda { Rack::Handler::Thin },
|
108
|
+
'mongrel' => lambda { Rack::Handler::Mongrel },
|
109
|
+
'unicorn' => lambda { Rack::Handler::Unicorn },
|
110
|
+
'rainbows' => lambda { Rack::Handler::Rainbows }
|
111
|
+
}[rack_require].call
|
112
|
+
|
83
113
|
handler.run( Rack::Lint.new(self.new), :Host => host, :Port => port )
|
114
|
+
|
84
115
|
end
|
85
116
|
|
86
|
-
def self.included(receiver)
|
117
|
+
def self.included( receiver )
|
87
118
|
receiver.extend( SingletonMethods )
|
88
119
|
end
|
89
120
|
|
121
|
+
# Generic 404 handler
|
90
122
|
def not_found
|
91
123
|
puts "/404: #{@request.fullpath.inspect}" if RSence.args[:verbose]
|
92
124
|
@response.status = 404
|
@@ -114,6 +146,7 @@ class Broker
|
|
114
146
|
|
115
147
|
end
|
116
148
|
|
149
|
+
### -- Add more http methods here when we have some apps to test with, caldav implementation maybe? ++
|
117
150
|
|
118
151
|
end
|
119
152
|
|
data/lib/http/rackup.rb
CHANGED
data/lib/http/request.rb
CHANGED
@@ -13,57 +13,55 @@ require 'rubygems'
|
|
13
13
|
require 'rack'
|
14
14
|
|
15
15
|
module RSence
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
16
|
+
|
17
|
+
# Simple Request class, slightly more involved
|
18
|
+
# than the Rack::Request it's extending.
|
19
|
+
class Request < Rack::Request
|
20
|
+
attr_reader :header, :path, :query
|
21
|
+
def initialize(env)
|
22
|
+
@header = {}
|
23
|
+
super
|
24
|
+
env2header()
|
25
|
+
@path = path_info()
|
26
|
+
@query = params()
|
27
|
+
end
|
28
|
+
def unparsed_uri
|
29
|
+
return @header['request-uri']
|
30
|
+
end
|
31
|
+
def env2header
|
32
|
+
[ ['SERVER_NAME', 'server-name'],
|
33
|
+
['HTTP_USER_AGENT', 'user-agent'],
|
34
|
+
['HTTP_ACCEPT_ENCODING', 'accept-encoding'],
|
35
|
+
['PATH_INFO', 'path-info'],
|
36
|
+
['HTTP_HOST', 'host'],
|
37
|
+
['HTTP_ACCEPT_LANGUAGE', 'accept-language'],
|
38
|
+
['SERVER_PROTOCOL', 'server-protocol'],
|
39
|
+
['REQUEST_PATH', 'request-path'],
|
40
|
+
['HTTP_KEEP_ALIVE', 'keep-alive'],
|
41
|
+
['SERVER_SOFTWARE', 'server-software'],
|
42
|
+
['REMOTE_ADDR', 'remote-addr'],
|
43
|
+
['HTTP_REFERER', 'referer'],
|
44
|
+
['HTTP_VERSION', 'version'],
|
45
|
+
['HTTP_ACCEPT_CHARSET', 'accept-charset'],
|
46
|
+
['REQUEST_URI', 'request-uri'],
|
47
|
+
['SERVER_PORT', 'server-port'],
|
48
|
+
['QUERY_STRING', 'query-string'],
|
49
|
+
['HTTP_ACCEPT', 'accept'],
|
50
|
+
['REQUEST_METHOD', 'request-method'],
|
51
|
+
['HTTP_CONNECTION', 'connection'],
|
52
|
+
['HTTP_SOAPACTION', 'soapaction'],
|
53
|
+
['HTTP_FORWARDED_HOST', 'forwarded-host']
|
54
|
+
].each do |env_key,header_key|
|
55
|
+
if @env.has_key?(env_key)
|
56
|
+
@header[header_key] = @env[env_key]
|
57
|
+
end
|
58
|
+
if env_key.start_with?( 'HTTP_' )
|
59
|
+
x_env_key = "HTTP_X#{env_key[4..-1]}"
|
60
|
+
if @env.has_key?( x_env_key )
|
61
|
+
@header["x-#{header_key}"] = @env[ x_env_key ]
|
62
|
+
end
|
62
63
|
end
|
63
64
|
end
|
64
65
|
end
|
65
66
|
end
|
66
67
|
end
|
67
|
-
|
68
|
-
|
69
|
-
end
|
data/lib/http/response.rb
CHANGED
@@ -10,54 +10,56 @@
|
|
10
10
|
|
11
11
|
|
12
12
|
module RSence
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
|
14
|
+
# Simply adds the + method "operator" to an extended Array.
|
15
|
+
# Used for pushing http body data.
|
16
|
+
class ResponseBody < Array
|
17
|
+
def +(body_data)
|
18
|
+
self.push(body_data)
|
19
|
+
end
|
17
20
|
end
|
18
|
-
end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
class Response
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
22
|
+
# Classic WEBrick -compatible Response object for Rack.
|
23
|
+
# Implements only the methods used by the framework.
|
24
|
+
class Response
|
25
|
+
def initialize
|
26
|
+
@body = ResponseBody.new(1)
|
27
|
+
@body[0] = ''
|
28
|
+
@status = 200
|
29
|
+
@header = {
|
30
|
+
'Content-Type' => 'text/plain',
|
31
|
+
'Server' => 'RSence'
|
32
|
+
}
|
33
|
+
end
|
34
|
+
def body=(body_data)
|
35
|
+
@body = ResponseBody.new(1)
|
36
|
+
@body[0] = body_data
|
37
|
+
end
|
38
|
+
def body
|
39
|
+
@body.join
|
40
|
+
end
|
41
|
+
def content_type=(new_content_type)
|
42
|
+
@header['Content-Type'] = new_content_type
|
43
|
+
end
|
44
|
+
def content_type
|
45
|
+
@header['Content-Type']
|
46
|
+
end
|
47
|
+
def camelize( header_key )
|
48
|
+
header_key.capitalize.gsub(/\-([a-z])/) { '-'+$1.upcase }
|
49
|
+
end
|
50
|
+
def []=(header_key,header_val)
|
51
|
+
@header[camelize( header_key )] = header_val.to_s
|
52
|
+
end
|
53
|
+
def status=(new_val)
|
54
|
+
@status = new_val.to_i
|
55
|
+
end
|
56
|
+
def status
|
57
|
+
@status
|
58
|
+
end
|
59
|
+
def header
|
60
|
+
@header
|
61
|
+
end
|
44
62
|
end
|
45
|
-
def camelize( header_key )
|
46
|
-
header_key.capitalize.gsub(/\-([a-z])/) { '-'+$1.upcase }
|
47
|
-
end
|
48
|
-
def []=(header_key,header_val)
|
49
|
-
@header[camelize( header_key )] = header_val.to_s
|
50
|
-
end
|
51
|
-
def status=(new_val)
|
52
|
-
@status = new_val.to_i
|
53
|
-
end
|
54
|
-
def status
|
55
|
-
@status
|
56
|
-
end
|
57
|
-
def header
|
58
|
-
@header
|
59
|
-
end
|
60
|
-
end
|
61
63
|
|
62
64
|
end
|
63
65
|
|
data/lib/plugins/gui_plugin.rb
CHANGED
@@ -6,28 +6,29 @@
|
|
6
6
|
# with this software package. If not, contact licensing@riassence.com
|
7
7
|
##
|
8
8
|
|
9
|
-
## The GUIPlugin extends Plugin by automatically initializing an GUIParser
|
10
|
-
## instance as @gui
|
11
|
-
## It makes the include_js method public to enable automatic dependency
|
12
|
-
## loading based on the dependencies item in the YAML gui declaration.
|
13
|
-
## It also makes the @path public.
|
14
|
-
## It inits the gui automatically.
|
15
|
-
## Extend the gui_params method to define your own params for the gui data.
|
16
|
-
##
|
17
|
-
## HValues can be defined inside values.yaml at the root directory of
|
18
|
-
## plugin. The HValues may be linked directly with methods on the values.yaml
|
19
|
-
## as well.
|
20
|
-
##
|
21
|
-
## == Values.yaml
|
22
|
-
## :valuename: # name of the HValue
|
23
|
-
## :value: 2.56 # defined value
|
24
|
-
## :responders: # methods responding to the value on ruby code upon change
|
25
|
-
## - :method: validate_convert_factor
|
26
|
-
##
|
27
|
-
##
|
28
|
-
##
|
29
9
|
module ::RSence
|
30
10
|
module Plugins
|
11
|
+
|
12
|
+
## The GUIPlugin extends Plugin by automatically initializing an GUIParser
|
13
|
+
## instance as @gui
|
14
|
+
## It makes the include_js method public to enable automatic dependency
|
15
|
+
## loading based on the dependencies item in the YAML gui declaration.
|
16
|
+
## It also makes the @path public.
|
17
|
+
## It inits the gui automatically.
|
18
|
+
## Extend the gui_params method to define your own params for the gui data.
|
19
|
+
##
|
20
|
+
## HValues can be defined inside values.yaml at the root directory of
|
21
|
+
## plugin. The HValues may be linked directly with methods on the values.yaml
|
22
|
+
## as well.
|
23
|
+
##
|
24
|
+
## == Values.yaml
|
25
|
+
## :valuename: # name of the HValue
|
26
|
+
## :value: 2.56 # defined value
|
27
|
+
## :responders: # methods responding to the value on ruby code upon change
|
28
|
+
## - :method: validate_convert_factor
|
29
|
+
##
|
30
|
+
##
|
31
|
+
##
|
31
32
|
class GUIPluginTemplate < PluginTemplate
|
32
33
|
def self.bundle_type; :GUIPlugin; end
|
33
34
|
|
@@ -35,7 +36,7 @@ module ::RSence
|
|
35
36
|
type: GUITree
|
36
37
|
version: 0.6
|
37
38
|
|
38
|
-
class:
|
39
|
+
class: RSence.GUIApp
|
39
40
|
options:
|
40
41
|
label: "Dummy Application"
|
41
42
|
|
data/lib/plugins/guiparser.rb
CHANGED
@@ -7,112 +7,113 @@
|
|
7
7
|
##
|
8
8
|
|
9
9
|
module ::RSence
|
10
|
-
module Plugins
|
11
|
-
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# @gui.
|
21
|
-
|
10
|
+
module Plugins
|
11
|
+
|
12
|
+
# This class automatically loads a YAML file from "gui" subdirectory of a plugin.
|
13
|
+
# Extend your plugin from the GUIPlugin class instead of the Plugin class to make
|
14
|
+
# this work automatically.
|
15
|
+
# = Usage:
|
16
|
+
# Initialize like this from inside a plugin method. This will load the "gui/my_gui.yaml" file.
|
17
|
+
# @gui = GUIParser.new( self, 'my_gui' )
|
18
|
+
# To make the client render the contents of the yaml do this:
|
19
|
+
# ses = get_ses( msg )
|
20
|
+
# params = { :values => @gui.values( ses ) }
|
21
|
+
# @gui.init( msg, params )
|
22
|
+
class GUIParser
|
22
23
|
|
23
|
-
|
24
|
+
include ::RSence
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
26
|
+
# Use this method to send the client all commands required to construct the GUI Tree using JSONRenderer.
|
27
|
+
# = Parameters
|
28
|
+
# +msg+:: The +Message+ instance +msg+ used all over the place.
|
29
|
+
# +params+:: An hash containing all parameters referred from the YAML file.
|
30
|
+
def init( msg, params )
|
31
|
+
gui_data = YAML.load( @yaml_src )
|
32
|
+
parse_gui( gui_data, params )
|
33
|
+
if gui_data.has_key?('dependencies')
|
34
|
+
@parent.include_js( msg, gui_data['dependencies'] )
|
35
|
+
gui_data.delete('dependencies')
|
36
|
+
end
|
37
|
+
if gui_data.has_key?('include')
|
38
|
+
gui_data['include'].each do | js_file |
|
39
|
+
js_src = @parent.read_js_once( msg, js_file )
|
40
|
+
msg.reply( js_src )
|
41
|
+
end
|
42
|
+
end
|
43
|
+
gui_name = @parent.name
|
44
|
+
json_data = JSON.dump( gui_data )
|
45
|
+
msg.reply( "JSONRenderer.nu(#{json_data});", true )
|
40
46
|
end
|
41
|
-
end
|
42
|
-
gui_name = @parent.name
|
43
|
-
json_data = JSON.dump( gui_data )
|
44
|
-
msg.reply( "JSONRenderer.nu(#{json_data});", true )
|
45
|
-
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
# Use this method to extract all the value id's of the +ses+ hash.
|
49
|
+
def values( ses )
|
50
|
+
ids = {}
|
51
|
+
ses.each do | key, value |
|
52
|
+
if value.class == HValue
|
53
|
+
ids[ key ] = value.val_id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
return ids
|
53
57
|
end
|
54
|
-
end
|
55
|
-
return ids
|
56
|
-
end
|
57
58
|
|
58
|
-
private
|
59
|
+
private
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
# Parses the gui data using params. Called from +init+.
|
65
|
-
def parse_gui( gui_data, params )
|
66
|
-
data_class = gui_data.class
|
67
|
-
if data_class == Array
|
68
|
-
gui_data.each_with_index do | item, i |
|
69
|
-
gui_data[i] = parse_gui( item, params )
|
70
|
-
end
|
71
|
-
elsif data_class == Hash
|
72
|
-
gui_data.each do | key, value |
|
73
|
-
gui_data[key] = parse_gui( value, params )
|
61
|
+
def json_fun( value )
|
62
|
+
JSON.parse( "[#{value}]" ).first
|
74
63
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
64
|
+
|
65
|
+
# Parses the gui data using params. Called from +init+.
|
66
|
+
def parse_gui( gui_data, params )
|
67
|
+
data_class = gui_data.class
|
68
|
+
if data_class == Array
|
69
|
+
gui_data.each_with_index do | item, i |
|
70
|
+
gui_data[i] = parse_gui( item, params )
|
71
|
+
end
|
72
|
+
elsif data_class == Hash
|
73
|
+
gui_data.each do | key, value |
|
74
|
+
gui_data[key] = parse_gui( value, params )
|
75
|
+
end
|
76
|
+
elsif data_class == Symbol
|
77
|
+
sym_str = gui_data.to_s
|
78
|
+
if sym_str.include? '.'
|
79
|
+
sym_arr = sym_str.split('.')
|
80
|
+
else
|
81
|
+
sym_arr = [ sym_str ]
|
82
|
+
end
|
83
|
+
return get_params( sym_arr, params )
|
84
|
+
elsif data_class == String and gui_data.strip.start_with?('function(')
|
85
|
+
return @parent.plugins[:client_pkg].squeeze( "a="+json_fun( gui_data.to_json ) )[2..-1]
|
86
|
+
end
|
87
|
+
return gui_data
|
81
88
|
end
|
82
|
-
return get_params( sym_arr, params )
|
83
|
-
elsif data_class == String and gui_data.strip.start_with?('function(')
|
84
|
-
return @parent.plugins[:client_pkg].squeeze( "a="+json_fun( gui_data.to_json ) )[2..-1]
|
85
|
-
end
|
86
|
-
return gui_data
|
87
|
-
end
|
88
89
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
90
|
+
# Searches the params hash for parameters whenever encountered a Symbol in the YAML.
|
91
|
+
def get_params( params_path, params )
|
92
|
+
item = params_path.shift
|
93
|
+
if params.class == Hash
|
94
|
+
has_str = params.has_key?( item )
|
95
|
+
has_sym = params.has_key?( item.to_sym )
|
96
|
+
item = item.to_sym if has_sym
|
97
|
+
if has_str or has_sym
|
98
|
+
if params_path.size == 0
|
99
|
+
return params[item]
|
100
|
+
else
|
101
|
+
return get_params( params_path, params[ item ] )
|
102
|
+
end
|
103
|
+
end
|
101
104
|
end
|
105
|
+
return ''
|
102
106
|
end
|
103
|
-
end
|
104
|
-
return ''
|
105
|
-
end
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
108
|
+
# Loads the YAML file.
|
109
|
+
# = Parameters
|
110
|
+
# +parent+:: The Plugin instance called from, use +self+ when constructing in a Plugin method.
|
111
|
+
# +yaml_src+:: The YAML source template for the GUI
|
112
|
+
def initialize( parent, yaml_src )
|
113
|
+
@parent = parent
|
114
|
+
@yaml_src = yaml_src
|
115
|
+
end
|
115
116
|
|
116
|
-
end
|
117
|
-
end
|
117
|
+
end
|
118
|
+
end
|
118
119
|
end
|