BuildMaster 0.5.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/README +25 -0
- data/lib/buildmaster.rb +9 -0
- data/lib/buildmaster/ant_client.rb +132 -0
- data/lib/buildmaster/build_file.rb +11 -0
- data/lib/buildmaster/cvs_client.rb +57 -0
- data/lib/buildmaster/file_processor.rb +62 -0
- data/lib/buildmaster/release_control.rb +19 -0
- data/lib/buildmaster/run_ant.rb +31 -0
- data/lib/buildmaster/shell_command.rb +38 -0
- data/lib/buildmaster/site.rb +147 -0
- data/lib/buildmaster/source_file_handler.rb +68 -0
- data/lib/buildmaster/svn_driver.rb +68 -0
- data/lib/buildmaster/template_runner.rb +128 -0
- data/lib/buildmaster/try.rb +3 -0
- data/lib/buildmaster/xtemplate.rb +28 -0
- data/lib/mock.rb +3 -0
- data/lib/mock/mock_base.rb +24 -0
- data/test/buildmaster/build.xml +8 -0
- data/test/buildmaster/content/index.html +7 -0
- data/test/buildmaster/tc_ant_client.rb +27 -0
- data/test/buildmaster/tc_cvs_client.rb +62 -0
- data/test/buildmaster/tc_release_control.rb +23 -0
- data/test/buildmaster/tc_site.rb +58 -0
- data/test/buildmaster/tc_svn_driver.rb +74 -0
- data/test/buildmaster/tc_template_runner.rb +48 -0
- data/test/buildmaster/tc_xtemplate.rb +256 -0
- data/test/buildmaster/template.xhtml +10 -0
- data/test/ts_buildmaster.rb +10 -0
- metadata +81 -0
data/README
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
= BuildMaster - Building and releasing project and site with Ruby
|
2
|
+
|
3
|
+
Homepage:: http://buildmaster.rubyforge.org
|
4
|
+
Author:: Shane
|
5
|
+
Copyright:: (c) 2006 BuildMaster on rubyforge
|
6
|
+
License:: Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt)
|
7
|
+
|
8
|
+
BuildMaster is a project that targets project building, releasing and site building.
|
9
|
+
|
10
|
+
Please post your question at http://groups.google.com/group/buildmaster/ or
|
11
|
+
send email to buildmaster@googlegroups.com
|
12
|
+
|
13
|
+
== Project Building and Releasing
|
14
|
+
|
15
|
+
A ruby version of the project building and releasing script as described in
|
16
|
+
"Pragmatic Project Automation" (http://www.pragmaticprogrammer.com/sk/auto/)
|
17
|
+
|
18
|
+
== Site Building
|
19
|
+
|
20
|
+
A simple template engine that can build a website by producing the content pages based on the specified source
|
21
|
+
and decrorating the content pages with a skin.
|
22
|
+
|
23
|
+
The supported content sources are <a href="http://www.w3.org/TR/xhtml11/">XHTML</a> and <a href="http://hobix.com/textile/">Textile</a>.
|
24
|
+
|
25
|
+
|
data/lib/buildmaster.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'buildmaster/shell_command'
|
4
|
+
require 'buildmaster/release_control'
|
5
|
+
require 'buildmaster/ant_client'
|
6
|
+
require 'buildmaster/cvs_client'
|
7
|
+
require 'buildmaster/svn_driver'
|
8
|
+
require 'buildmaster/site'
|
9
|
+
require 'buildmaster/xtemplate'
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class Ant
|
2
|
+
include Shell
|
3
|
+
def initialize(ant_file = nil, &command_runner)
|
4
|
+
@ant_file = ant_file
|
5
|
+
@ant_home = check_directory(get_environment('ANT_HOME'))
|
6
|
+
#ISSUE: what java wants to split up classpath varies from platform to platform
|
7
|
+
#and ruby, following perl, is not too hot at hinting which box it is on.
|
8
|
+
#here I assume ":" except on win32, dos, and netware. Add extra tests here as needed.
|
9
|
+
#This is the only way I know how to check if on windows
|
10
|
+
if File.directory?('C:')
|
11
|
+
@class_path_delimiter = ';'
|
12
|
+
@path_seperator='\\'
|
13
|
+
else
|
14
|
+
@class_path_delimiter = ':'
|
15
|
+
@path_seperator='/'
|
16
|
+
end
|
17
|
+
@java_command = get_java_command()
|
18
|
+
@ant_options = get_environment_or_default('ANT_OPTS').split(' ')
|
19
|
+
@ant_arguments = get_environment_or_default('ANT_ARGS').split(' ')
|
20
|
+
@ant_options.putsh(ENV['JIKESPATH']) if ENV['JIKESPATH']
|
21
|
+
@classpath=ENV['CLASSPATH']
|
22
|
+
@command_runner = command_runner
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_java_command
|
26
|
+
java_home = get_environment_or_default('JAVA_HOME')
|
27
|
+
if java_home
|
28
|
+
command = [java_home, 'bin', 'java'].join(@path_seperator)
|
29
|
+
else
|
30
|
+
command = get_environment_or_default('JAVA_CMD', 'java')
|
31
|
+
end
|
32
|
+
return command
|
33
|
+
end
|
34
|
+
|
35
|
+
def project_help
|
36
|
+
launch('-projecthelp')
|
37
|
+
end
|
38
|
+
|
39
|
+
def target(name)
|
40
|
+
launch(name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def launch(arguments)
|
44
|
+
local_path = "#{@ant_home}/lib/ant-launcher.jar"
|
45
|
+
if (@classpath and @classpath.length() > 0)
|
46
|
+
local_path = "#{local_path}#{@class_path_delimiter}#{@classpath}"
|
47
|
+
end
|
48
|
+
all_arguments = Array.new()
|
49
|
+
all_arguments.push(@ant_options)
|
50
|
+
all_arguments.push('-classpath', local_path)
|
51
|
+
all_arguments.push("-Dant.home=#{@ant_home}")
|
52
|
+
all_arguments.push('org.apache.tools.ant.launch.Launcher', @ant_arguments);
|
53
|
+
all_arguments.push('-f', @ant_file) if @ant_file
|
54
|
+
all_arguments.push(arguments)
|
55
|
+
command_line = "#{@java_command} #{all_arguments.join(' ')}"
|
56
|
+
run_command(command_line)
|
57
|
+
end
|
58
|
+
|
59
|
+
private :get_java_command
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class RubyAnt
|
64
|
+
def initialize(ant_file = nil, &command_runner)
|
65
|
+
@ant_file = ant_file
|
66
|
+
@ant_home = check_directory(get_environment('ANT_HOME'))
|
67
|
+
@java_command = get_environment_or_default('JAVA_CMD', 'java')
|
68
|
+
#ISSUE: what java wants to split up classpath varies from platform to platform
|
69
|
+
#and ruby, following perl, is not too hot at hinting which box it is on.
|
70
|
+
#here I assume ":" except on win32, dos, and netware. Add extra tests here as needed.
|
71
|
+
#This is the only way I know how to check if on windows
|
72
|
+
if File.directory?('C:')
|
73
|
+
@class_path_delimiter = ';'
|
74
|
+
else
|
75
|
+
@class_path_delimiter = ':'
|
76
|
+
end
|
77
|
+
@ant_options = get_environment_or_default('ANT_OPTS').split(' ')
|
78
|
+
@ant_arguments = get_environment_or_default('ANT_ARGS').split(' ')
|
79
|
+
@ant_options.putsh(ENV['JIKESPATH']) if ENV['JIKESPATH']
|
80
|
+
@classpath=ENV['CLASSPATH']
|
81
|
+
@command_runner = command_runner
|
82
|
+
end
|
83
|
+
|
84
|
+
def launch(arguments)
|
85
|
+
local_path = "#{@ant_home}/lib/ant-launcher.jar"
|
86
|
+
if (@classpath and @classpath.length() > 0)
|
87
|
+
local_path = "#{local_path}#{@class_path_delimiter}#{@classpath}"
|
88
|
+
end
|
89
|
+
all_arguments = Array.new()
|
90
|
+
all_arguments.push(@ant_options)
|
91
|
+
all_arguments.push('-classpath', local_path)
|
92
|
+
all_arguments.push("-Dant.home=#{@ant_home}")
|
93
|
+
all_arguments.push('org.apache.tools.ant.launch.Launcher', @ant_arguments);
|
94
|
+
all_arguments.push('-f', @ant_file) if @ant_file
|
95
|
+
all_arguments.push(arguments)
|
96
|
+
command_line = "#{@java_command} #{all_arguments.join(' ')}"
|
97
|
+
run_command(command_line)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def run(command)
|
102
|
+
if (!system(command))
|
103
|
+
raise "error running command: #{command}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_command(full_command)
|
108
|
+
if (@command_runner)
|
109
|
+
@command_runner.call(full_command)
|
110
|
+
else
|
111
|
+
run(full_command)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
def check_directory(path)
|
117
|
+
raise "#{path} is not a directory" unless File.directory? path
|
118
|
+
return path
|
119
|
+
end
|
120
|
+
|
121
|
+
def get_environment(name)
|
122
|
+
value = ENV[name]
|
123
|
+
raise "#{name} is not set" unless value
|
124
|
+
return value
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_environment_or_default(name, default_value='')
|
128
|
+
value = ENV[name]
|
129
|
+
value = default_value if not value
|
130
|
+
return value
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class CvsInfo
|
2
|
+
attr_reader :root, :repository
|
3
|
+
|
4
|
+
public
|
5
|
+
def initialize(root, repository)
|
6
|
+
@root = root
|
7
|
+
@repository = repository
|
8
|
+
end
|
9
|
+
|
10
|
+
def CvsInfo.load(folder)
|
11
|
+
return CvsInfo.new(read("#{folder}/Root").strip!, read("#{folder}/Repository").strip!)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def CvsInfo.read(file)
|
16
|
+
File.open(file) do |file|
|
17
|
+
return file.gets
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class CvsClient
|
24
|
+
include Shell
|
25
|
+
def CvsClient.load(working_directory)
|
26
|
+
return CvsClient.new(CvsInfo.load("#{working_directory}/CVS"), working_directory)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(cvs_info, working_directory, &command_runner)
|
30
|
+
@cvs_info = cvs_info
|
31
|
+
@working_directory = working_directory
|
32
|
+
@command_runner = command_runner
|
33
|
+
end
|
34
|
+
|
35
|
+
def command(command)
|
36
|
+
run_command "cvs -d #{@cvs_info.root} #{command} #{@working_directory}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def checkout()
|
40
|
+
run_command("cvs -d #{@cvs_info.root} co -d #{@working_directory} #{@cvs_info.repository}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def update(option='')
|
44
|
+
if (option.length > 0)
|
45
|
+
option = ' ' + option
|
46
|
+
end
|
47
|
+
command("update#{option}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def tag(name)
|
51
|
+
command("tag #{name}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def commit(comment)
|
55
|
+
command("commit -m \"#{comment}\"")
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module BuildMaster
|
4
|
+
|
5
|
+
class FileProcessor
|
6
|
+
def initialize(template, content_path, evaluator)
|
7
|
+
@template = template
|
8
|
+
@content_path = content_path
|
9
|
+
@evaluator = evaluator
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_textile()
|
13
|
+
textile_content = IO.read(@content_path)
|
14
|
+
match_result = TEXTILE_REGX.match(textile_content)
|
15
|
+
title = ''
|
16
|
+
if match_result != nil
|
17
|
+
title = match_result[2]
|
18
|
+
textile_content = match_result.post_match
|
19
|
+
end
|
20
|
+
html_body = RedCloth.new(textile_content).to_html
|
21
|
+
html_content = <<HTML
|
22
|
+
<!DOCTYPE html
|
23
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
24
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
25
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
26
|
+
<head>
|
27
|
+
<title>#{title}</title>
|
28
|
+
</head>
|
29
|
+
<body>
|
30
|
+
#{html_body}
|
31
|
+
</body>
|
32
|
+
</html>
|
33
|
+
HTML
|
34
|
+
return process_html_content(html_content)
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_html()
|
38
|
+
return process_html_content(File.open(@content_path))
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def process_html_content(source)
|
43
|
+
document_with_skin = @template.process(source) do |message|
|
44
|
+
begin
|
45
|
+
method = @evaluator.method(message)
|
46
|
+
if (method.arity == 0)
|
47
|
+
method.call
|
48
|
+
else
|
49
|
+
method.call(@content_path)
|
50
|
+
end
|
51
|
+
rescue NameError
|
52
|
+
raise TemplateException,
|
53
|
+
"unable to process message: #{message}: #{$!}" ,
|
54
|
+
caller
|
55
|
+
end
|
56
|
+
end
|
57
|
+
return document_with_skin
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Release
|
2
|
+
def initialize(vcs_driver, builder)
|
3
|
+
@vcs_driver = vcs_driver
|
4
|
+
@builder = builder
|
5
|
+
end
|
6
|
+
|
7
|
+
def release_candidate(tag)
|
8
|
+
@vcs_driver.checkout
|
9
|
+
@vcs_driver.tag(tag)
|
10
|
+
@builder.build
|
11
|
+
@vcs_driver.commit
|
12
|
+
end
|
13
|
+
|
14
|
+
def release_rebuild(tag)
|
15
|
+
@vcs_driver.checkout(tag)
|
16
|
+
@builder.invoke
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
#######################################################################
|
4
|
+
=begin
|
5
|
+
runant.rb
|
6
|
+
|
7
|
+
This script is a translation of the runant.pl written by Steve Loughran.
|
8
|
+
It runs ant with/out arguments
|
9
|
+
This script has been tested with ruby1.8.2-14/WinXP
|
10
|
+
|
11
|
+
created: 2005-03-08
|
12
|
+
author: Shane Duan
|
13
|
+
=end
|
14
|
+
|
15
|
+
# Debugging flag
|
16
|
+
$debug = true
|
17
|
+
|
18
|
+
# Assumptions:
|
19
|
+
#
|
20
|
+
# - the "java" executable/script is on the command path
|
21
|
+
# - ANT_HOME has been set
|
22
|
+
# - target platform uses ":" as classpath separator or a "C:" exists (windows)
|
23
|
+
# - target platform uses "/" as directory separator.
|
24
|
+
|
25
|
+
#######################################################################
|
26
|
+
require 'Ant'
|
27
|
+
|
28
|
+
if not Ant.new().launch(ARGV)
|
29
|
+
raise 'Ant failed'
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Shell
|
2
|
+
private
|
3
|
+
def run(command)
|
4
|
+
if (!system(command))
|
5
|
+
raise "error running command: #{command}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_command(full_command)
|
10
|
+
if (@command_runner)
|
11
|
+
@command_runner.call(full_command)
|
12
|
+
else
|
13
|
+
run(full_command)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def check_directory(path)
|
18
|
+
raise "#{path} is not a directory" unless File.directory? path
|
19
|
+
return path
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_environment(name)
|
23
|
+
value = ENV[name]
|
24
|
+
raise "#{name} is not set" unless value
|
25
|
+
return value
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_environment_or_default(name, default_value='')
|
29
|
+
value = ENV[name]
|
30
|
+
value = default_value if not value
|
31
|
+
return value
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
class SystemCommandRunner
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'redcloth'
|
5
|
+
require 'webrick'
|
6
|
+
require 'source_file_handler'
|
7
|
+
require 'file_processor'
|
8
|
+
|
9
|
+
module BuildMaster
|
10
|
+
#todo match only beginning of the file
|
11
|
+
TEXTILE_REGX = /---(-)*\n(.*)\n(-)*---/
|
12
|
+
|
13
|
+
class SiteSpec
|
14
|
+
def self.get_instance
|
15
|
+
self.new()
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :output_dir, :content_dir, :template, :template_file
|
19
|
+
|
20
|
+
def validate_inputs
|
21
|
+
validate_dir(@content_dir, :content_dir)
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_template
|
25
|
+
if (@template)
|
26
|
+
return XTemplate.new(@template)
|
27
|
+
else
|
28
|
+
return XTemplate.new(File.open(@template_file))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def validate_dir(directory, symbol)
|
34
|
+
if not directory
|
35
|
+
raise "Directory for #{symbol.id2name} not specified"
|
36
|
+
end
|
37
|
+
if not File.exists? directory
|
38
|
+
raise "Directory for #{symbol.id2name} -- <#{directory}#> does not exist"
|
39
|
+
end
|
40
|
+
if not File.directory? directory
|
41
|
+
raise "<#{directory}> should be a directory for #{symbol.id2name}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_file(file, symbol)
|
46
|
+
if not file
|
47
|
+
raise "File for #{symbol.id2name} not specified"
|
48
|
+
end
|
49
|
+
if (not File.exists? file)
|
50
|
+
raise "File for #{symbol.id2name} -- <#{file}> does not exist"
|
51
|
+
end
|
52
|
+
if not File.file? file
|
53
|
+
raise "#<{file} should be a file for #{symbol.id2name}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Site
|
59
|
+
def initialize(spec)
|
60
|
+
@spec = spec
|
61
|
+
@spec.validate_inputs
|
62
|
+
@template = @spec.load_template
|
63
|
+
end
|
64
|
+
|
65
|
+
public
|
66
|
+
def execute(arguments)
|
67
|
+
action = 'build'
|
68
|
+
if arguments.size > 0
|
69
|
+
action = arguments[0]
|
70
|
+
end
|
71
|
+
method(action).call
|
72
|
+
end
|
73
|
+
|
74
|
+
def build
|
75
|
+
@count = 0
|
76
|
+
ensure_directory_exists(@spec.output_dir)
|
77
|
+
build_directory(@spec.output_dir, @spec.content_dir, @template)
|
78
|
+
puts "Generated file count: #{@count}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def server
|
82
|
+
mime_types = WEBrick::HTTPUtils::DefaultMimeTypes.update(
|
83
|
+
{"textile" => "text/plain"}
|
84
|
+
)
|
85
|
+
server = WEBrick::HTTPServer.new(
|
86
|
+
:Port => 2000,
|
87
|
+
:Logger => WEBrick::Log.new($stdout, WEBrick::Log::INFO),
|
88
|
+
:MimeTypes => mime_types
|
89
|
+
)
|
90
|
+
server.mount('/', SourceFileHandler, @spec)
|
91
|
+
server.mount('/source', WEBrick::HTTPServlet::FileHandler, @spec.content_dir, true)
|
92
|
+
['INT', 'TERM'].each { |signal|
|
93
|
+
trap(signal){ server.shutdown}
|
94
|
+
}
|
95
|
+
server.start
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def ensure_directory_exists(dir_name)
|
100
|
+
if (not File.exist?(dir_name))
|
101
|
+
ensure_directory_exists(File.join(dir_name, '..'))
|
102
|
+
Dir.mkdir dir_name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_directory(out_dir, content_dir, template)
|
107
|
+
Dir.foreach(content_dir) do |item|
|
108
|
+
content_path = File.join(content_dir, item)
|
109
|
+
if (item == '.' || item == '..' || item == '.svn' || item == 'CVS')
|
110
|
+
elsif (File.directory? content_path)
|
111
|
+
build_directory(File.join(out_dir, item), content_path, template)
|
112
|
+
else
|
113
|
+
@current_file_name = content_path
|
114
|
+
process_file(content_path, out_dir, content_dir, item)
|
115
|
+
@count = @count + 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def process_file(content_path, out_dir, content_dir, item)
|
121
|
+
print ">> #{content_path}"
|
122
|
+
extension = File.extname(content_path)
|
123
|
+
isIndex = @current_file_name =~ /^index/
|
124
|
+
file_processor = FileProcessor.new(@template, content_path, @spec)
|
125
|
+
if (extension.casecmp('.textile') == 0)
|
126
|
+
base_name = File.basename(item, extension)
|
127
|
+
output_file = File.join(out_dir, base_name + '.html')
|
128
|
+
write(output_file, file_processor.process_textile())
|
129
|
+
elsif (extension.casecmp('.html') == 0)
|
130
|
+
output_file = File.join(out_dir, item)
|
131
|
+
write(output_file, file_processor.process_html())
|
132
|
+
else
|
133
|
+
output_file = File.join(out_dir, item)
|
134
|
+
puts " ==> #{output_file}"
|
135
|
+
FileUtils.cp content_path, output_file
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def write(target_file, xhtml)
|
140
|
+
puts " ==> #{target_file}"
|
141
|
+
File.open(target_file, "w") do |file|
|
142
|
+
xhtml.write(file, 0, false, true)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|