langrove 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.rspec +1 -0
  2. data/.rvmrc +62 -0
  3. data/.watchr +27 -0
  4. data/Gemfile +16 -0
  5. data/Gemfile.lock +61 -0
  6. data/Rakefile +24 -0
  7. data/functional/.gitignore +1 -0
  8. data/functional/bin/datagram +6 -0
  9. data/functional/config/.gitignore +0 -0
  10. data/functional/config/boot.rb +64 -0
  11. data/functional/config/daemons.yml +12 -0
  12. data/functional/config/environment.rb +28 -0
  13. data/functional/config/environments/development.rb +0 -0
  14. data/functional/config/environments/production.rb +0 -0
  15. data/functional/config/environments/test.rb +0 -0
  16. data/functional/lib/daemon/datagram.rb +21 -0
  17. data/functional/lib/handler/socket_to_file.rb +36 -0
  18. data/functional/lib/protocol/socket_to_file.rb +55 -0
  19. data/functional/libexec/daemon.rb +68 -0
  20. data/functional/log/.gitignore +3 -0
  21. data/functional/tmp/README +1 -0
  22. data/lib/langrove/_base.rb +26 -0
  23. data/lib/langrove/adaptor/base.rb +3 -0
  24. data/lib/langrove/adaptor/datagram.rb +27 -0
  25. data/lib/langrove/adaptor_base.rb +89 -0
  26. data/lib/langrove/client/base.rb +2 -0
  27. data/lib/langrove/client/datagram.rb +25 -0
  28. data/lib/langrove/client_base.rb +114 -0
  29. data/lib/langrove/daemon/base.rb +2 -0
  30. data/lib/langrove/daemon_base.rb +175 -0
  31. data/lib/langrove/ext/class_loader.rb +148 -0
  32. data/lib/langrove/ext/config_item.rb +34 -0
  33. data/lib/langrove/ext/config_loader.rb +16 -0
  34. data/lib/langrove/ext/fake_logger.rb +8 -0
  35. data/lib/langrove/ext/persistable.rb +103 -0
  36. data/lib/langrove/ext/string.rb +35 -0
  37. data/lib/langrove/ext.rb +7 -0
  38. data/lib/langrove/handler/base.rb +2 -0
  39. data/lib/langrove/handler_base.rb +141 -0
  40. data/lib/langrove/protocol/base.rb +2 -0
  41. data/lib/langrove/protocol/syslog.rb +32 -0
  42. data/lib/langrove/protocol_base.rb +32 -0
  43. data/lib/langrove/version.rb +3 -0
  44. data/lib/langrove.rb +1 -0
  45. data/spec/functional/daemon/datagram_spec.rb +115 -0
  46. data/spec/langrove/adaptor/datagram_spec.rb +6 -0
  47. data/spec/langrove/adaptor_base_spec.rb +48 -0
  48. data/spec/langrove/client/datagram_spec.rb +1 -0
  49. data/spec/langrove/client_base_spec.rb +5 -0
  50. data/spec/langrove/daemon_base_spec.rb +101 -0
  51. data/spec/langrove/ext/class_loader_spec.rb +83 -0
  52. data/spec/langrove/ext/config_item_spec.rb +81 -0
  53. data/spec/langrove/ext/config_loader_spec.rb +5 -0
  54. data/spec/langrove/ext/fake_logger_spec.rb +0 -0
  55. data/spec/langrove/ext/persistable_spec.rb +117 -0
  56. data/spec/langrove/ext/string_spec.rb +16 -0
  57. data/spec/langrove/handler_base_spec.rb +57 -0
  58. data/spec/langrove/protocol/syslog_spec.rb +45 -0
  59. data/spec/langrove/protocol_base_spec.rb +6 -0
  60. data/spec/todo_spec.rb +12 -0
  61. data/tmp/README +2 -0
  62. metadata +200 -0
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
7
+ environment_id="ruby-1.9.2-p0@langrove"
8
+
9
+ #
10
+ # Uncomment following line if you want options to be set only for given project.
11
+ #
12
+ # PROJECT_JRUBY_OPTS=( --1.9 )
13
+
14
+ #
15
+ # First we attempt to load the desired environment directly from the environment
16
+ # file. This is very fast and efficient compared to running through the entire
17
+ # CLI and selector. If you want feedback on which environment was used then
18
+ # insert the word 'use' after --create as this triggers verbose mode.
19
+ #
20
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
21
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
22
+ then
23
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
24
+
25
+ if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
26
+ then
27
+ . "${rvm_path:-$HOME/.rvm}/hooks/after_use"
28
+ fi
29
+ else
30
+ # If the environment file has not yet been created, use the RVM CLI to select.
31
+ if ! rvm --create use "$environment_id"
32
+ then
33
+ echo "Failed to create RVM environment '${environment_id}'."
34
+ return 1
35
+ fi
36
+ fi
37
+
38
+ #
39
+ # If you use an RVM gemset file to install a list of gems (*.gems), you can have
40
+ # it be automatically loaded. Uncomment the following and adjust the filename if
41
+ # necessary.
42
+ #
43
+ # filename=".gems"
44
+ # if [[ -s "$filename" ]]
45
+ # then
46
+ # rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
47
+ # fi
48
+
49
+ # If you use bundler, this might be useful to you:
50
+ # if command -v bundle && [[ -s Gemfile ]]
51
+ # then
52
+ # bundle install
53
+ # fi
54
+
55
+ if [[ $- == *i* ]] # check for interactive shells
56
+ then
57
+ echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
58
+ else
59
+ echo "Using: $GEM_HOME" # don't use colors in interactive shells
60
+ fi
61
+
62
+
data/.watchr ADDED
@@ -0,0 +1,27 @@
1
+ def run_spec(file)
2
+ unless File.exist?(file)
3
+ puts "#{file} does not exist"
4
+ return
5
+ end
6
+
7
+ puts "Running #{file}"
8
+ system "rspec #{file}"
9
+ puts
10
+ end
11
+
12
+ watch("spec/.*/*_spec.rb") do |match|
13
+ run_spec match[0]
14
+ end
15
+
16
+ watch("(.*/*).rb") do |match|
17
+ file = match[1]
18
+ file.gsub!('lib/','')
19
+ file.gsub!('spec/','')
20
+ file.sub!('_spec','')
21
+ puts "====#{file}==="
22
+ if /demo/.match(match[0]) then
23
+ run_spec %{spec/#{file}_spec.rb}
24
+ next
25
+ end
26
+ run_spec %{spec/#{file}_spec.rb}
27
+ end
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source :gemcutter
2
+ source "http://rubygems.org"
3
+
4
+ gem 'watchr'
5
+ gem 'rspec'
6
+
7
+ # daemon-kit
8
+ gem 'daemon-kit'
9
+ gem 'eventmachine'
10
+ gem 'em-http-request'
11
+ gem 'awesome_print'
12
+
13
+ gem 'eventmachine_httpserver'
14
+
15
+ gem 'resque'
16
+ gem 'rake'
data/Gemfile.lock ADDED
@@ -0,0 +1,61 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ remote: http://rubygems.org/
4
+ specs:
5
+ addressable (2.2.7)
6
+ awesome_print (1.0.2)
7
+ daemon-kit (0.1.8.2)
8
+ eventmachine (>= 0.12.10)
9
+ safely (>= 0.3.1)
10
+ diff-lcs (1.1.3)
11
+ em-http-request (0.3.0)
12
+ addressable (>= 2.0.0)
13
+ escape_utils
14
+ eventmachine (>= 0.12.9)
15
+ escape_utils (0.2.4)
16
+ eventmachine (0.12.10)
17
+ eventmachine_httpserver (0.2.1)
18
+ multi_json (1.3.2)
19
+ rack (1.4.1)
20
+ rack-protection (1.2.0)
21
+ rack
22
+ rake (0.8.7)
23
+ redis (2.2.2)
24
+ redis-namespace (1.0.3)
25
+ redis (< 3.0.0)
26
+ resque (1.20.0)
27
+ multi_json (~> 1.0)
28
+ redis-namespace (~> 1.0.2)
29
+ sinatra (>= 0.9.2)
30
+ vegas (~> 0.1.2)
31
+ rspec (2.9.0)
32
+ rspec-core (~> 2.9.0)
33
+ rspec-expectations (~> 2.9.0)
34
+ rspec-mocks (~> 2.9.0)
35
+ rspec-core (2.9.0)
36
+ rspec-expectations (2.9.1)
37
+ diff-lcs (~> 1.1.3)
38
+ rspec-mocks (2.9.0)
39
+ safely (0.3.1)
40
+ sinatra (1.3.2)
41
+ rack (~> 1.3, >= 1.3.6)
42
+ rack-protection (~> 1.2)
43
+ tilt (~> 1.3, >= 1.3.3)
44
+ tilt (1.3.3)
45
+ vegas (0.1.11)
46
+ rack (>= 1.0.0)
47
+ watchr (0.7)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ awesome_print
54
+ daemon-kit
55
+ em-http-request
56
+ eventmachine
57
+ eventmachine_httpserver
58
+ rake
59
+ resque
60
+ rspec
61
+ watchr
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+
3
+ desc "Publish a new version to ruby-gems.org"
4
+ task :publish do
5
+ require 'langrove/version'
6
+
7
+ sh "gem build langrove.gemspec"
8
+ sh "gem push langrove-#{LanGrove::Version}.gem"
9
+
10
+ sh "git tag v#{LanGrove::Version}"
11
+ sh "git push origin v#{LanGrove::Version}"
12
+ sh "git push origin master"
13
+ sh "git clean -fd"
14
+
15
+ # exec "rake documentation"
16
+
17
+ end
18
+
19
+ desc "Push to github"
20
+ task :push_world do
21
+
22
+ puts "TODO"
23
+
24
+ end
@@ -0,0 +1 @@
1
+ tmp/*.*
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ DAEMON_ROOT = File.expand_path('../../../functional/', __FILE__)
4
+ DAEMON_NAME = File.basename $0
5
+ require File.expand_path('../../config/environment', __FILE__)
6
+ DaemonKit::Application.exec( DAEMON_ROOT + '/libexec/daemon.rb' )
File without changes
@@ -0,0 +1,64 @@
1
+ # Don't change this file!
2
+ # Configure your daemon in config/environment.rb
3
+
4
+ DAEMON_ROOT = "#{File.expand_path(File.dirname(__FILE__))}/.." unless defined?( DAEMON_ROOT )
5
+
6
+ require "rubygems"
7
+ require "bundler/setup"
8
+
9
+ module DaemonKit
10
+ class << self
11
+ def boot!
12
+ unless booted?
13
+ pick_boot.run
14
+ end
15
+ end
16
+
17
+ def booted?
18
+ defined? DaemonKit::Initializer
19
+ end
20
+
21
+ def pick_boot
22
+ (vendor_kit? ? VendorBoot : GemBoot).new
23
+ end
24
+
25
+ def vendor_kit?
26
+ File.exists?( "#{DAEMON_ROOT}/vendor/daemon-kit" )
27
+ end
28
+ end
29
+
30
+ class Boot
31
+ def run
32
+ load_initializer
33
+ DaemonKit::Initializer.run
34
+ end
35
+ end
36
+
37
+ class VendorBoot < Boot
38
+ def load_initializer
39
+ require "#{DAEMON_ROOT}/vendor/daemon-kit/lib/daemon_kit/initializer"
40
+ end
41
+ end
42
+
43
+ class GemBoot < Boot
44
+ def load_initializer
45
+ begin
46
+ require 'rubygems' unless defined?( ::Gem )
47
+ gem 'daemon-kit'
48
+ require 'daemon_kit/initializer'
49
+ rescue ::Gem::LoadError => e
50
+ msg = <<EOF
51
+
52
+ You are missing the daemon-kit gem. Please install the following gem:
53
+
54
+ sudo gem install daemon-kit
55
+
56
+ EOF
57
+ $stderr.puts msg
58
+ exit 1
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ DaemonKit.boot!
@@ -0,0 +1,12 @@
1
+ :daemons:
2
+ datagram:
3
+ :periodic: 5
4
+ :adaptor:
5
+ :connection: Datagram
6
+ :iface: 127.0.0.1
7
+ :port: 12701
8
+ :client:
9
+ :class: Datagram
10
+ :handler:
11
+ :collection: SocketToFile
12
+ :protocol: SocketToFile
@@ -0,0 +1,28 @@
1
+ ENV["TZ"] = "ZA"
2
+ #ENV['DAEMON_ENV'] ||= 'production'
3
+
4
+ # Boot up
5
+ require File.join(File.dirname(__FILE__), 'boot')
6
+
7
+ # Auto-require default libraries.
8
+ Bundler.require :default, DaemonKit.env
9
+
10
+ DaemonKit::Initializer.run do |config|
11
+
12
+ config.daemon_name = DAEMON_NAME
13
+
14
+ # Force the daemon to be killed after X seconds from asking it to
15
+ # config.force_kill_wait = 30
16
+
17
+ # Log backraces when a thread/daemon dies (Recommended)
18
+ config.backtraces = true
19
+
20
+ #
21
+ # TODO: move log level into per daemon config
22
+ #
23
+ #config.log_level = :debug
24
+ config.log_level = :info
25
+ #config.log_level = :warn
26
+ #config.log_level = :error
27
+
28
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,21 @@
1
+ class Datagram < LanGrove::Daemon::Base
2
+
3
+ #
4
+ # Consider tossing this dependancy.
5
+ #
6
+ # Not entirely certain this layer in the abstraction
7
+ # will be useful.
8
+ #
9
+ # Except, by
10
+ #
11
+ # << using this layer in the abstraction >>
12
+ #
13
+ #
14
+ # the eval "require 'module/class_name.rb'" in the
15
+ # langrove/ext/class_loader could possibly be avoided
16
+ # if the implementation requires all necessary modules
17
+ # ahead of starting the daemon.
18
+ #
19
+ #
20
+
21
+ end
@@ -0,0 +1,36 @@
1
+ #
2
+ # Define a Handler by extending LanGrove::Handler::Base
3
+ # and extending the functions:
4
+ #
5
+ # 1. <YourClass>.receive( data )
6
+ #
7
+ # Data arriving from the Adaptor will arrive into
8
+ # this function having already been decoded by the
9
+ # configured Protocol for this Handler
10
+ #
11
+ # # 2. <YourClass>.transmit
12
+ # #
13
+
14
+ require 'langrove'
15
+
16
+ module Handler
17
+
18
+ class SocketToFile < LanGrove::Handler::Base
19
+
20
+ def receive( data )
21
+
22
+ @logger.info( "#{self}.receive: #{data}" )
23
+
24
+ File.open( data[:filename], 'w' ) do |f|
25
+
26
+ f.write( data[:content] )
27
+
28
+ end
29
+
30
+ @logger.info "Wrote data: '#{data[ :content ]}' to file: '#{ data[ :filename ]}'"
31
+
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,55 @@
1
+ #
2
+ # Define a Protocol by extending LanGrove::Protocol::Base
3
+ # and extending the functions:
4
+ #
5
+ # 1. <YourClass>.decode( data )
6
+ #
7
+ # Data arriving from the Adaptor will be passed to
8
+ # the config assigned protocol. The Handler will
9
+ # be expecting decoded data in the form of a Hash
10
+ #
11
+ # SPECIFICALLY: One of the keys should correspond
12
+ # to the :route_by in the config.
13
+ #
14
+ # # 2. <YourClass>.encode( .. )
15
+ # #
16
+
17
+
18
+ require 'langrove'
19
+
20
+ module Protocol
21
+
22
+ #
23
+ # Module is only necessary if you choose to
24
+ # structure associations of class by name.
25
+ #
26
+ # eg.
27
+ #
28
+ # Protocol::SocketToFile
29
+ # Handler::SocketToFile
30
+ #
31
+
32
+ class SocketToFile < LanGrove::Protocol::Base
33
+
34
+ def decode( data )
35
+
36
+ #
37
+ # OVERRIDE
38
+ #
39
+ # Becasuse your protocol
40
+ #
41
+
42
+ @logger.info( "#{self}.decode: #{data}" ) unless @logger.nil?
43
+
44
+ {
45
+
46
+ :filename => data.split('|')[0],
47
+ :content => data.split('|',2)[1]
48
+
49
+ }
50
+
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,68 @@
1
+ require 'langrove'
2
+
3
+ CLASS_NAME = DAEMON_NAME.camelize
4
+ CONFIG_FILE = "#{DAEMON_ROOT}/config/daemons.yml"
5
+ CONFIG_HASH = LanGrove::ConfigLoader.yaml( CONFIG_FILE )
6
+
7
+ daemon = nil
8
+
9
+ DaemonKit::Application.running! do |config|
10
+
11
+ DaemonKit.logger.info "Spawing #{DAEMON_NAME} at pidfile #{config.pid_file}"
12
+
13
+ #
14
+ # setup signal traps
15
+ #
16
+ config.trap( 'TERM', Proc.new {
17
+
18
+ #
19
+ # Handle stop signal from os
20
+ #
21
+ daemon.stop_daemon
22
+
23
+ } )
24
+
25
+ config.trap( 'INT', Proc.new {
26
+
27
+ #
28
+ # Handle ^C from user (eg. developer) running un-daemonized
29
+ #
30
+ daemon.stop_daemon
31
+
32
+ } )
33
+
34
+ #
35
+ # Have not yet '''suitably''' succeeded in getting a sigHUP
36
+ # into a daemon with the monit/daemonkit combo yet....
37
+ #
38
+ # grumbles...
39
+ #
40
+ config.trap( 'HUP', Proc.new {
41
+
42
+ #
43
+ # Handle reload
44
+ #
45
+ daemon.reload_daemon
46
+
47
+ } )
48
+
49
+ #
50
+ # Latebind the daemon class
51
+ #
52
+ # TODO: see about tossing the libexec
53
+ # or modifying class loader to enable
54
+ # alternate load path to allow this pattern:
55
+ #
56
+ # daemon = LanGrove::ClassLoader.create( {
57
+ #
58
+ # :module => 'Daemon',
59
+ # :class => DAEMON_NAME.camelize
60
+ #
61
+ # } ).new( CONFIG_HASH, DAEMON_NAME, DaemonKit.logger )
62
+ #
63
+
64
+ require "daemon/#{DAEMON_NAME}"
65
+ daemon = Object.const_get( CLASS_NAME ).new( CONFIG_HASH, DAEMON_NAME, DaemonKit.logger )
66
+ daemon.run
67
+
68
+ end
@@ -0,0 +1,3 @@
1
+ *.log
2
+ *.pid
3
+ *.yml
@@ -0,0 +1 @@
1
+ Certain test write files into here
@@ -0,0 +1,26 @@
1
+ #$LOAD_PATH.unshift 'langrove' unless $LOAD_PATH.include?( 'langrove' )
2
+
3
+ #$LOAD_PATH.inspect
4
+
5
+ #
6
+ # For the implementation client
7
+ #
8
+ module Adaptor; end
9
+ module Client; end
10
+ module Daemon; end
11
+ module Handler; end
12
+ module Protocol; end
13
+
14
+ module LanGrove
15
+
16
+ class DaemonConfigException < Exception; end
17
+
18
+ end
19
+
20
+ require 'langrove/ext'
21
+
22
+ require 'langrove/daemon/base'
23
+ require 'langrove/adaptor/base'
24
+ require 'langrove/handler/base'
25
+ require 'langrove/protocol/base'
26
+ require 'langrove/client/base'
@@ -0,0 +1,3 @@
1
+ module LanGrove::Adaptor; end
2
+ require 'langrove/adaptor_base'
3
+ require 'langrove/adaptor/datagram'
@@ -0,0 +1,27 @@
1
+ require 'langrove/adaptor/base'
2
+
3
+ module LanGrove::Adaptor
4
+
5
+ class Datagram < Base
6
+
7
+ def listen( handler, protocol )
8
+
9
+ @logger.info "starting listen at UDP #{@iface}:#{@port}"
10
+
11
+ EventMachine::open_datagram_socket( @iface, @port,
12
+
13
+ @client ) do |client|
14
+
15
+ @logger.info "client assign handler: #{handler}"
16
+ client.handler = handler
17
+
18
+ @logger.info "client instanciate protocol: #{protocol}"
19
+ client.protocol = protocol.new( nil, @logger )
20
+
21
+ @logger.info "client assign config: #{@client_config}"
22
+ client.config = @client_config.clone
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,89 @@
1
+ require 'eventmachine'
2
+
3
+ require 'langrove'
4
+
5
+ module LanGrove::Adaptor
6
+
7
+ class Base
8
+
9
+ def initialize( config, logger )
10
+
11
+ @config = config
12
+ @logger = logger
13
+
14
+ @iface = '127.0.0.1'
15
+ @port = 12701
16
+
17
+ @iface = @config[ :iface ] if @config.has_key? :iface
18
+ @port = @config[ :port ] if @config.has_key? :port
19
+
20
+ if @config.has_key? :connector then
21
+
22
+ #
23
+ # TODO: may need to override default connection handler
24
+ #
25
+
26
+ end
27
+
28
+ #
29
+ # initialize the client specified in config
30
+ #
31
+ # daemons:
32
+ # name_of_daemon:
33
+ # adaptor:
34
+ # connection: TcpServer
35
+ # client:
36
+ # class: Client <---------
37
+ # handler:
38
+ # collection: CollectionOfClients
39
+ #
40
+ #
41
+
42
+ @logger.info "TODO: loading client is not tested"
43
+
44
+ client = nil
45
+
46
+ begin
47
+
48
+ #
49
+ # TODO: make this more informative on error
50
+ #
51
+
52
+ @client_config = @config[ :client ]
53
+ client = @client_config[ :class ]
54
+
55
+ rescue
56
+
57
+ error = "Missing config item(s) for adaptor: #{@config.inspect}"
58
+
59
+ @logger.error "EXIT: #{error}"
60
+
61
+ raise LanGrove::DaemonConfigException.new(
62
+
63
+ "Missing config item for daemon: #{@daemon_name}"
64
+
65
+ )
66
+
67
+ end
68
+
69
+ @logger.info "Load definition: Client::#{client}"
70
+
71
+ @client = LanGrove::ClassLoader.create( {
72
+
73
+ :module => 'Client',
74
+ :class => client
75
+
76
+ } )
77
+
78
+
79
+ end
80
+
81
+ def listen( handler, protocol )
82
+
83
+ raise LanGrove::DaemonConfigException.new( "NotYetExtended: undefined listen()" )
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,2 @@
1
+ module LanGrove::Client; end
2
+ require 'langrove/client_base'
@@ -0,0 +1,25 @@
1
+ module LanGrove
2
+
3
+ module Client
4
+
5
+ class Datagram < Base
6
+
7
+ def receive( data )
8
+
9
+ #
10
+ # Quick hack, datagram client routes
11
+ # data back to the Handler - to get
12
+ # back to friday position.
13
+ #
14
+ # But with the gem
15
+ #
16
+
17
+ @handler.receive( data )
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end