rave 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/rave +27 -23
- data/lib/commands/appcfg.rb +9 -0
- data/lib/commands/create.rb +153 -147
- data/lib/commands/server.rb +7 -7
- data/lib/commands/task.rb +156 -0
- data/lib/commands/usage.rb +18 -12
- data/lib/commands/war.rb +27 -50
- data/lib/exceptions.rb +19 -5
- data/lib/ext/logger.rb +7 -0
- data/lib/gems.yaml +9 -0
- data/lib/jars/{appengine-api-1.0-sdk-1.2.1.jar → appengine-api-1.0-sdk-1.3.0.jar} +0 -0
- data/lib/mixins/controller.rb +72 -40
- data/lib/mixins/data_format.rb +206 -168
- data/lib/mixins/logger.rb +19 -0
- data/lib/mixins/object_factory.rb +87 -0
- data/lib/mixins/time_utils.rb +19 -0
- data/lib/models/annotation.rb +148 -18
- data/lib/models/blip.rb +305 -61
- data/lib/models/component.rb +42 -0
- data/lib/models/context.rb +174 -45
- data/lib/models/document.rb +8 -8
- data/lib/models/element.rb +113 -0
- data/lib/models/event.rb +230 -48
- data/lib/models/operation.rb +79 -89
- data/lib/models/range.rb +14 -0
- data/lib/models/robot.rb +78 -60
- data/lib/models/user.rb +62 -0
- data/lib/models/wave.rb +45 -19
- data/lib/models/wavelet.rb +269 -69
- data/lib/ops/blip_ops.rb +233 -134
- data/lib/rave.rb +27 -22
- metadata +96 -77
- data/lib/jars/jruby-core.jar +0 -0
- data/lib/jars/ruby-stdlib.jar +0 -0
data/bin/rave
CHANGED
@@ -1,24 +1,28 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
here = File.dirname(__FILE__)
|
4
|
-
%w( create server usage war ).each do |cmd|
|
5
|
-
require File.join(here, "..", "lib", "commands", cmd)
|
6
|
-
end
|
7
|
-
|
8
|
-
args = ARGV
|
9
|
-
cmd = args.shift
|
10
|
-
|
11
|
-
if cmd
|
12
|
-
case cmd
|
13
|
-
when "create"
|
14
|
-
create_robot(args)
|
15
|
-
when "server"
|
16
|
-
start_robot(args)
|
17
|
-
when "war"
|
18
|
-
create_war(args)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
here = File.dirname(__FILE__)
|
4
|
+
%w( create server usage war appcfg ).each do |cmd|
|
5
|
+
require File.join(here, "..", "lib", "commands", cmd)
|
6
|
+
end
|
7
|
+
|
8
|
+
args = ARGV
|
9
|
+
cmd = args.shift
|
10
|
+
|
11
|
+
if cmd
|
12
|
+
case cmd
|
13
|
+
when "create"
|
14
|
+
create_robot(args)
|
15
|
+
when "server"
|
16
|
+
start_robot(args)
|
17
|
+
when "war"
|
18
|
+
create_war(args)
|
19
|
+
when "cleanup"
|
20
|
+
cleanup_war(args)
|
21
|
+
when "appengine_deploy"
|
22
|
+
appcfg_update(args)
|
23
|
+
else
|
24
|
+
display_usage
|
25
|
+
end
|
26
|
+
else
|
27
|
+
display_usage
|
24
28
|
end
|
data/lib/commands/create.rb
CHANGED
@@ -1,147 +1,153 @@
|
|
1
|
-
require 'ftools'
|
2
|
-
|
3
|
-
#Creates a project for a robot. Args are:
|
4
|
-
# => robot_name (required)
|
5
|
-
# => image_url=http://imageurl.com/ (optional)
|
6
|
-
# => profile_url=http://profileurl.com/ (optional)
|
7
|
-
# e.g. rave my_robot image_url=http://appropriate-casey.appspot.com/image.png profile_url=http://appropriate-casey.appspot.com/profile.json
|
8
|
-
def create_robot(args)
|
9
|
-
robot_name = args.first
|
10
|
-
module_name = robot_name.split(/_|-/).collect { |word| word
|
11
|
-
robot_class_name = "#{module_name}::Robot"
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
File.
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
File.
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
puts "Creating
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
# def
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
1
|
+
require 'ftools'
|
2
|
+
|
3
|
+
#Creates a project for a robot. Args are:
|
4
|
+
# => robot_name (required)
|
5
|
+
# => image_url=http://imageurl.com/icon.png (optional)
|
6
|
+
# => profile_url=http://profileurl.com/ (optional)
|
7
|
+
# e.g. rave my_robot image_url=http://appropriate-casey.appspot.com/image.png profile_url=http://appropriate-casey.appspot.com/profile.json
|
8
|
+
def create_robot(args)
|
9
|
+
robot_name = args.first
|
10
|
+
module_name = robot_name.split(/_|-/).collect { |word| word.capitalize }.join
|
11
|
+
robot_class_name = "#{module_name}::Robot"
|
12
|
+
|
13
|
+
options = { :name => robot_name, :version => 1, :id => "#{robot_name}@appspot.com" }
|
14
|
+
args[1, args.length-1].each do |arg|
|
15
|
+
key, value = arg.split("=").collect { |part| part.strip }
|
16
|
+
options[key.to_sym] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
dir = File.join(".", robot_name)
|
20
|
+
if File.exist? dir
|
21
|
+
puts "Directory #{dir}/ already exists. Exiting..."
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
lib = File.join(dir, "lib")
|
26
|
+
config_dir = File.join(dir, "config")
|
27
|
+
file = File.join(dir, "robot.rb")
|
28
|
+
run = File.join(dir, "config.ru")
|
29
|
+
config = File.join(dir, "config.yaml")
|
30
|
+
public_folder = File.join(dir, "public")
|
31
|
+
html = File.join(public_folder, "index.html")
|
32
|
+
|
33
|
+
#Create the project dir
|
34
|
+
puts "Creating directory #{File.expand_path(dir)}"
|
35
|
+
Dir.mkdir(dir)
|
36
|
+
|
37
|
+
puts "Creating robot class #{File.expand_path(file)}"
|
38
|
+
#Make the base robot class
|
39
|
+
File.open(file, "w") do |f|
|
40
|
+
f.puts robot_file_contents(module_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Make the rackup run file.
|
44
|
+
puts "Creating rackup config file #{File.expand_path(run)}"
|
45
|
+
File.open(run, "w") do |f|
|
46
|
+
f.puts run_file_contents(robot_class_name, file)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Make up the yaml config file.
|
50
|
+
puts "Creating configuration file #{File.expand_path(config)}"
|
51
|
+
File.open(config, "w") do |f|
|
52
|
+
f.puts config_file_contents(options)
|
53
|
+
end
|
54
|
+
|
55
|
+
#Make the public folder for static resources
|
56
|
+
puts "Creating public folder"
|
57
|
+
Dir.mkdir(public_folder)
|
58
|
+
|
59
|
+
# Make up the html index file.
|
60
|
+
puts "Creating html index file #{File.expand_path(html)}"
|
61
|
+
File.open(html, "w") do |f|
|
62
|
+
f.puts html_file_contents(robot_name, options[:id], options[:image_url])
|
63
|
+
end
|
64
|
+
|
65
|
+
#Create lib directory
|
66
|
+
puts "Creating lib directory #{File.expand_path(lib)}"
|
67
|
+
Dir.mkdir(lib)
|
68
|
+
end
|
69
|
+
|
70
|
+
def robot_file_contents(module_name)
|
71
|
+
<<-ROBOT
|
72
|
+
require 'rubygems'
|
73
|
+
require 'rave'
|
74
|
+
|
75
|
+
module #{module_name}
|
76
|
+
class Robot < Rave::Models::Robot
|
77
|
+
#Define handlers here:
|
78
|
+
# e.g. if the robot should act on a DOCUMENT_CHANGED event:
|
79
|
+
#
|
80
|
+
# def document_changed(event, context)
|
81
|
+
# #Do some stuff
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# Events are:
|
85
|
+
#
|
86
|
+
# WAVELET_BLIP_CREATED, WAVELET_BLIP_REMOVED, WAVELET_PARTICIPANTS_CHANGED,
|
87
|
+
# WAVELET_TIMESTAMP_CHANGED, WAVELET_TITLE_CHANGED, WAVELET_VERSION_CHANGED,
|
88
|
+
# BLIP_CONTRIBUTORS_CHANGED, BLIP_DELETED, BLIP_SUBMITTED, BLIP_TIMESTAMP_CHANGED,
|
89
|
+
# BLIP_VERSION_CHANGED, DOCUMENT_CHANGED, FORM_BUTTON_CLICKED
|
90
|
+
#
|
91
|
+
# If you want to name your event handler something other than the default name,
|
92
|
+
# or you need to have more than one handler for an event, you can register handlers
|
93
|
+
# in the robot's constructor:
|
94
|
+
#
|
95
|
+
# def initialize(options={})
|
96
|
+
# super
|
97
|
+
# register_handler(Rave::Models::Event::DOCUMENT_CHANGED, :custom_doc_changed_handler)
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# def custom_doc_changed_handler(event, context)
|
101
|
+
# #Do some stuff
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# Note: Don't forget to call super if you define #initialize
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
ROBOT
|
109
|
+
end
|
110
|
+
|
111
|
+
def run_file_contents(robot_class_name, robot_file)
|
112
|
+
<<-CONFIG
|
113
|
+
require '#{File.basename(robot_file).chomp(File.extname(robot_file))}'
|
114
|
+
run #{robot_class_name}.instance
|
115
|
+
CONFIG
|
116
|
+
end
|
117
|
+
|
118
|
+
def config_file_contents(options)
|
119
|
+
<<-CONFIG
|
120
|
+
robot:
|
121
|
+
id: #{options[:id]}
|
122
|
+
name: #{options[:name]}
|
123
|
+
image_url: #{options[:image_url]}
|
124
|
+
profile_url: #{options[:profile_url]}
|
125
|
+
version: #{options[:version]}
|
126
|
+
appcfg:
|
127
|
+
version: 1
|
128
|
+
# Uncomment this section to add gems required by your robot.
|
129
|
+
# They will unpacked in your project at appengine deploy time
|
130
|
+
# gems:
|
131
|
+
# - some_gem # Replace this with the name of the gem you require
|
132
|
+
# - some_other_gem # Replace this with the name of the gem you require
|
133
|
+
CONFIG
|
134
|
+
end
|
135
|
+
|
136
|
+
def html_file_contents(name, id, image_url)
|
137
|
+
img_tag = image_url ? "\n <img src=\"#{image_url}\" alt=\"#{name} icon\" />\n" : ""
|
138
|
+
<<-HTML
|
139
|
+
<html>
|
140
|
+
<head>
|
141
|
+
<title>#{name}</title>
|
142
|
+
</head>
|
143
|
+
<body>
|
144
|
+
<h1>#{name}</h1>
|
145
|
+
#{img_tag}
|
146
|
+
<p>This is a Google Wave robot using <a href="http://github.com/diminish7/rave">Rave</a> running in JRuby.
|
147
|
+
Use this robot in your Google Waves by adding <em>#{id}</em> as a participant</p>
|
148
|
+
|
149
|
+
<img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif" alt="Powered by Google App Engine" />
|
150
|
+
</body>
|
151
|
+
</html>
|
152
|
+
HTML
|
153
|
+
end
|
data/lib/commands/server.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
#Starts up rack based on the config.ru file in the working directory
|
2
|
-
# Note that this is of limited use right now, because robots have to
|
3
|
-
# run on appengine. Better to test locally with the appengine sdk
|
4
|
-
def start_robot(args)
|
5
|
-
cmd = (RUBY_PLATFORM == 'java') ? "jruby -S rackup" : "rackup"
|
6
|
-
cmd += " " + args.join(" ") if args
|
7
|
-
exec(cmd)
|
1
|
+
#Starts up rack based on the config.ru file in the working directory
|
2
|
+
# Note that this is of limited use right now, because robots have to
|
3
|
+
# run on appengine. Better to test locally with the appengine sdk
|
4
|
+
def start_robot(args)
|
5
|
+
cmd = (RUBY_PLATFORM == 'java') ? "jruby -S rackup" : "rackup"
|
6
|
+
cmd += " " + args.join(" ") if args
|
7
|
+
exec(cmd)
|
8
8
|
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'yaml'
|
5
|
+
require 'warbler'
|
6
|
+
|
7
|
+
module Rave
|
8
|
+
class Task < Warbler::Task
|
9
|
+
|
10
|
+
REQUIRED_GEMS = ["rave", "json-jruby", "rack", "builder", "RedCloth"]
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
warbler_config = Warbler::Config.new do |config|
|
14
|
+
config.gems = ((robot_config['gems'] || []) + REQUIRED_GEMS).uniq
|
15
|
+
config.includes = %w( robot.rb config.yaml )
|
16
|
+
end
|
17
|
+
super(:rave, warbler_config)
|
18
|
+
define_post_war_processes
|
19
|
+
define_deploy_task
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def robot_config
|
25
|
+
@robot_config ||= YAML::load(File.open(File.join(".", "config.yaml")))
|
26
|
+
end
|
27
|
+
|
28
|
+
def define_post_war_processes
|
29
|
+
namespace :rave do
|
30
|
+
desc "Post-War cleanup"
|
31
|
+
task :create_war => 'rave' do
|
32
|
+
#TODO: This needs to only run through this if the files have changed
|
33
|
+
#Get config info
|
34
|
+
web_inf = File.join(".", "tmp", "war", "WEB-INF")
|
35
|
+
rave_jars = File.join(File.dirname(__FILE__), "..", "jars")
|
36
|
+
#Cleanup unneeded gems that warbler copies in
|
37
|
+
cleanup_gems(File.join(web_inf, "gems", "gems"), robot_config['gems'] || [])
|
38
|
+
#Copy the appengine sdk jar to the robot
|
39
|
+
copy_appengine_jar_to_robot(rave_jars, File.join(web_inf, "lib"))
|
40
|
+
#Fix the broken paths in json-jruby
|
41
|
+
fix_json_jruby_paths(File.join(web_inf, "gems", "gems"))
|
42
|
+
#Add the appengine-web.xml file
|
43
|
+
robot_name = robot_config['robot']['id'].gsub(/@.+/, '')
|
44
|
+
version = robot_config['appcfg'] && robot_config['appcfg']['version'] ? robot_config['appcfg']['version'] : 1
|
45
|
+
create_appengine_web(File.join(web_inf, "appengine-web.xml"), robot_name, version)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def define_deploy_task
|
51
|
+
namespace :rave do
|
52
|
+
desc "Deploy to Appengine"
|
53
|
+
task :appcfg_update => :create_war do
|
54
|
+
staging_folder = File.join(".", "tmp", "war")
|
55
|
+
sdk_path = find_sdk
|
56
|
+
if sdk_path
|
57
|
+
appcfg_jar = File.expand_path(File.join(sdk_path, 'lib', 'appengine-tools-api.jar'))
|
58
|
+
require appcfg_jar
|
59
|
+
Java::ComGoogleAppengineToolsAdmin::AppCfg.main(["update", staging_folder].to_java(:string))
|
60
|
+
else
|
61
|
+
puts "Unable to find the Google Appengine Java SDK"
|
62
|
+
puts "You can either"
|
63
|
+
puts "1. Define the path to the main SDK folder in config.yaml - e.g.:"
|
64
|
+
puts "appcfg:"
|
65
|
+
puts " sdk: /usr/local/appengine-java-sdk/"
|
66
|
+
puts "2. Add the SDK bin folder to your PATH, or"
|
67
|
+
puts "3. Create an environment variable APPENGINE_JAVA_SDK that defines the path to the main SDK folder"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
#Remove warbler and jruby-jars - added by warbler but unneeded
|
74
|
+
def cleanup_gems(gem_dir, gems)
|
75
|
+
["warbler", "jruby-jars"].each do |g|
|
76
|
+
dir = Dir[File.join(gem_dir, "#{g}*")].first
|
77
|
+
unless dir.nil? || gems.include?(g)
|
78
|
+
puts "Removing #{g} from war"
|
79
|
+
FileUtils.rm_rf(dir)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def copy_appengine_jar_to_robot(rave_jar_dir, warbler_jar_dir)
|
85
|
+
jar = "appengine-api-1.0-sdk-1.3.0.jar"
|
86
|
+
rave_jar = File.join(rave_jar_dir, jar)
|
87
|
+
warbler_jar = File.join(warbler_jar_dir, jar)
|
88
|
+
puts "Copying appengine jar from #{rave_jar} to #{warbler_jar}"
|
89
|
+
File.copy(rave_jar, warbler_jar)
|
90
|
+
end
|
91
|
+
|
92
|
+
def fix_json_jruby_paths(web_inf_gems)
|
93
|
+
#TODO: Why is this necessary? Is this an appengine issue?
|
94
|
+
puts "Fixing paths in json-jruby"
|
95
|
+
ext = Dir[File.join(web_inf_gems, "json-jruby-*", "lib", "json", "ext.rb")].first
|
96
|
+
if ext
|
97
|
+
text = File.open(ext, "r") { |f| f.read }
|
98
|
+
text.gsub!("require 'json/ext/parser'", "require 'ext/parser'")
|
99
|
+
text.gsub!("require 'json/ext/generator'", "require 'ext/generator'")
|
100
|
+
File.open(ext, "w") { |f| f.write(text) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_appengine_web(path, robot_name, version)
|
105
|
+
puts "Creating appengine config file #{File.expand_path(path)}"
|
106
|
+
File.open(path, "w") do |f|
|
107
|
+
f.puts appengine_web_contents(robot_name, version)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def appengine_web_contents(robot_name, version)
|
112
|
+
<<-APPENGINE
|
113
|
+
<?xml version="1.0" encoding="utf-8"?>
|
114
|
+
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
115
|
+
<application>#{robot_name}</application>
|
116
|
+
<version>#{version}</version>
|
117
|
+
<static-files />
|
118
|
+
<resource-files />
|
119
|
+
<sessions-enabled>false</sessions-enabled>
|
120
|
+
<system-properties>
|
121
|
+
<property name="jruby.management.enabled" value="false" />
|
122
|
+
<property name="os.arch" value="" />
|
123
|
+
<property name="jruby.compile.mode" value="JIT"/> <!-- JIT|FORCE|OFF -->
|
124
|
+
<property name="jruby.compile.fastest" value="true"/>
|
125
|
+
<property name="jruby.compile.frameless" value="true"/>
|
126
|
+
<property name="jruby.compile.positionless" value="true"/>
|
127
|
+
<property name="jruby.compile.threadless" value="false"/>
|
128
|
+
<property name="jruby.compile.fastops" value="false"/>
|
129
|
+
<property name="jruby.compile.fastcase" value="false"/>
|
130
|
+
<property name="jruby.compile.chainsize" value="500"/>
|
131
|
+
<property name="jruby.compile.lazyHandles" value="false"/>
|
132
|
+
<property name="jruby.compile.peephole" value="true"/>
|
133
|
+
</system-properties>
|
134
|
+
</appengine-web-app>
|
135
|
+
APPENGINE
|
136
|
+
end
|
137
|
+
|
138
|
+
def find_sdk
|
139
|
+
unless @sdk_path
|
140
|
+
@sdk_path = robot_config['appcfg']['sdk'] if robot_config['appcfg'] && robot_config['appcfg']['sdk'] # Points at main SDK dir.
|
141
|
+
@sdk_path ||= ENV['APPENGINE_JAVA_SDK'] # Points at main SDK dir.
|
142
|
+
unless @sdk_path
|
143
|
+
# Check everything in the PATH, which would point at the bin directory in the SDK.
|
144
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
145
|
+
if File.exists?(File.join(path, "appcfg.sh")) or File.exists?(File.join("appcfg.cmd"))
|
146
|
+
@sdk_path = File.dirname(path)
|
147
|
+
break
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
@sdk_path
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|