activeldap 3.1.1 → 3.2.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/Gemfile +1 -14
- data/benchmark/README.md +64 -0
- data/benchmark/{bench-al.rb → bench-backend.rb} +6 -22
- data/benchmark/bench-instantiate.rb +98 -0
- data/benchmark/config.yaml.sample +2 -2
- data/doc/text/news.textile +38 -0
- data/lib/active_ldap.rb +17 -8
- data/lib/active_ldap/association/has_many_wrap.rb +15 -2
- data/lib/active_ldap/attribute_methods.rb +23 -0
- data/lib/active_ldap/attribute_methods/before_type_cast.rb +24 -0
- data/lib/active_ldap/attribute_methods/dirty.rb +43 -0
- data/lib/active_ldap/attribute_methods/query.rb +31 -0
- data/lib/active_ldap/attribute_methods/read.rb +44 -0
- data/lib/active_ldap/attribute_methods/write.rb +38 -0
- data/lib/active_ldap/attributes.rb +18 -26
- data/lib/active_ldap/base.rb +42 -163
- data/lib/active_ldap/connection.rb +6 -1
- data/lib/active_ldap/get_text.rb +18 -7
- data/lib/active_ldap/operations.rb +63 -49
- data/lib/active_ldap/persistence.rb +17 -0
- data/lib/active_ldap/railtie.rb +3 -0
- data/lib/active_ldap/schema.rb +2 -0
- data/lib/active_ldap/schema/syntaxes.rb +7 -7
- data/lib/active_ldap/validations.rb +2 -2
- data/lib/active_ldap/version.rb +3 -0
- data/lib/active_ldap/xml.rb +24 -7
- data/lib/rails/generators/active_ldap/model/model_generator.rb +3 -3
- data/test/add-phonetic-attribute-options-to-slapd.ldif +10 -0
- data/test/al-test-utils.rb +428 -0
- data/test/command.rb +111 -0
- data/test/config.yaml.sample +6 -0
- data/test/fixtures/lower_case_object_class_schema.rb +802 -0
- data/test/run-test.rb +29 -0
- data/test/test_associations.rb +37 -0
- data/test/test_base.rb +113 -51
- data/test/test_dirty.rb +84 -0
- data/test/test_ldif.rb +0 -1
- data/test/test_load.rb +0 -1
- data/test/test_reflection.rb +7 -14
- data/test/test_syntax.rb +104 -43
- data/test/test_usermod-binary-del.rb +1 -1
- data/test/test_usermod-lang-add.rb +0 -1
- metadata +272 -224
- data/lib/active_ldap/get_text_fallback.rb +0 -60
- data/lib/active_ldap/get_text_support.rb +0 -22
data/Gemfile
CHANGED
@@ -2,17 +2,4 @@
|
|
2
2
|
|
3
3
|
source "http://rubygems.org"
|
4
4
|
|
5
|
-
|
6
|
-
gem 'locale'
|
7
|
-
gem 'fast_gettext'
|
8
|
-
gem 'gettext_i18n_rails'
|
9
|
-
|
10
|
-
group :development, :test do
|
11
|
-
gem 'ruby-ldap'
|
12
|
-
gem 'net-ldap'
|
13
|
-
gem 'jeweler'
|
14
|
-
gem 'test-unit'
|
15
|
-
gem 'test-unit-notify'
|
16
|
-
gem "yard"
|
17
|
-
gem "RedCloth"
|
18
|
-
end
|
5
|
+
gemspec
|
data/benchmark/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# README
|
2
|
+
|
3
|
+
This document describes how to run benchmarks under
|
4
|
+
benchmark/ directory.
|
5
|
+
|
6
|
+
## Configure your LDAP server
|
7
|
+
|
8
|
+
You need a LDAP server to run benchmarks. This is dependes
|
9
|
+
on your environment.
|
10
|
+
|
11
|
+
In this document, we assume that you configure your LDAP
|
12
|
+
server by the following configuration:
|
13
|
+
|
14
|
+
* host: 127.0.0.1
|
15
|
+
* base DN: dc=bench,dc=local
|
16
|
+
* encryption: startTLS
|
17
|
+
* bind DN: cn=admin,dc=local
|
18
|
+
* password: secret
|
19
|
+
|
20
|
+
## Configure ActiveLdap to connect to your LDAP server
|
21
|
+
|
22
|
+
You need an ActiveLdap configuration in
|
23
|
+
benchmark/config.yaml to connect to your LDAP server. There
|
24
|
+
is a sample configuration in
|
25
|
+
benchmark/config.yaml.sample. It's good to start from it.
|
26
|
+
|
27
|
+
% cp benchmark/config.yaml.sample benchmark/config.yaml
|
28
|
+
% editor benchmark/config.yaml
|
29
|
+
|
30
|
+
The configuration uses the same format of ldap.yaml.
|
31
|
+
|
32
|
+
## Run benchmarks
|
33
|
+
|
34
|
+
You just run a bencmark script. It loads
|
35
|
+
benchmark/config.yaml and populate benchmark data automatically.
|
36
|
+
|
37
|
+
% ruby benchmark/bench-backend.rb
|
38
|
+
Populating...
|
39
|
+
|
40
|
+
Rehearsal ---------------------------------------------------------------
|
41
|
+
1x: AL(LDAP) 0.220000 0.000000 0.220000 ( 0.234775)
|
42
|
+
1x: AL(Net::LDAP) 0.280000 0.000000 0.280000 ( 0.273048)
|
43
|
+
1x: AL(LDAP: No Obj) 0.000000 0.000000 0.000000 ( 0.009217)
|
44
|
+
1x: AL(Net::LDAP: No Obj) 0.060000 0.000000 0.060000 ( 0.056727)
|
45
|
+
1x: LDAP 0.000000 0.000000 0.000000 ( 0.003261)
|
46
|
+
1x: Net::LDAP 0.040000 0.000000 0.040000 ( 0.029862)
|
47
|
+
------------------------------------------------------ total: 0.600000sec
|
48
|
+
|
49
|
+
user system total real
|
50
|
+
1x: AL(LDAP) 0.200000 0.000000 0.200000 ( 0.195660)
|
51
|
+
1x: AL(Net::LDAP) 0.220000 0.000000 0.220000 ( 0.213444)
|
52
|
+
1x: AL(LDAP: No Obj) 0.010000 0.000000 0.010000 ( 0.009000)
|
53
|
+
1x: AL(Net::LDAP: No Obj) 0.030000 0.000000 0.030000 ( 0.026847)
|
54
|
+
1x: LDAP 0.000000 0.000000 0.000000 ( 0.003377)
|
55
|
+
1x: Net::LDAP 0.020000 0.000000 0.020000 ( 0.022662)
|
56
|
+
|
57
|
+
Entries processed by Ruby/ActiveLdap + LDAP: 100
|
58
|
+
Entries processed by Ruby/ActiveLdap + Net::LDAP: 100
|
59
|
+
Entries processed by Ruby/ActiveLdap + LDAP: (without object creation): 100
|
60
|
+
Entries processed by Ruby/ActiveLdap + Net::LDAP: (without object creation): 100
|
61
|
+
Entries processed by Ruby/LDAP: 100
|
62
|
+
Entries processed by Net::LDAP: 100
|
63
|
+
|
64
|
+
Cleaning...
|
@@ -7,7 +7,11 @@ require "benchmark"
|
|
7
7
|
|
8
8
|
include ActiveLdap::GetTextSupport
|
9
9
|
|
10
|
-
argv
|
10
|
+
argv = ARGV.dup
|
11
|
+
unless argv.include?("--config")
|
12
|
+
argv.unshift("--config", File.join(base, "config.yaml"))
|
13
|
+
end
|
14
|
+
argv, opts, options = ActiveLdap::Command.parse_options(argv) do |opts, options|
|
11
15
|
options.prefix = "ou=People"
|
12
16
|
|
13
17
|
opts.on("--prefix=PREFIX",
|
@@ -129,27 +133,7 @@ rescue LoadError
|
|
129
133
|
end
|
130
134
|
|
131
135
|
def populate_base
|
132
|
-
|
133
|
-
ActiveLdap::Base.base.split(/,/).reverse_each do |suffix|
|
134
|
-
prefix = suffixes.join(",")
|
135
|
-
suffixes.unshift(suffix)
|
136
|
-
name, value = suffix.split(/=/, 2)
|
137
|
-
next unless name == "dc"
|
138
|
-
dc_class = Class.new(ActiveLdap::Base)
|
139
|
-
dc_class.ldap_mapping :dn_attribute => "dc",
|
140
|
-
:prefix => "",
|
141
|
-
:scope => :base,
|
142
|
-
:classes => ["top", "dcObject", "organization"]
|
143
|
-
dc_class.instance_variable_set("@base", prefix)
|
144
|
-
next if dc_class.exists?(value, :prefix => "dc=#{value}")
|
145
|
-
dc = dc_class.new(value)
|
146
|
-
dc.o = dc.dc
|
147
|
-
begin
|
148
|
-
dc.save
|
149
|
-
rescue ActiveLdap::OperationNotPermitted
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
136
|
+
ActiveLdap::Populate.ensure_base
|
153
137
|
if ActiveLdap::Base.search.empty?
|
154
138
|
raise "Can't populate #{ActiveLdap::Base.base}"
|
155
139
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
base = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(File.expand_path(base))
|
3
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(base, "..", "lib")))
|
4
|
+
|
5
|
+
require "active_ldap"
|
6
|
+
require "benchmark"
|
7
|
+
|
8
|
+
include ActiveLdap::GetTextSupport
|
9
|
+
|
10
|
+
argv = ARGV.dup
|
11
|
+
unless argv.include?("--config")
|
12
|
+
argv.unshift("--config", File.join(base, "config.yaml"))
|
13
|
+
end
|
14
|
+
argv, opts, options = ActiveLdap::Command.parse_options(argv) do |opts, options|
|
15
|
+
options.prefix = "ou=People"
|
16
|
+
|
17
|
+
opts.on("--prefix=PREFIX",
|
18
|
+
_("Specify prefix for benchmarking"),
|
19
|
+
_("(default: %s)") % options.prefix) do |prefix|
|
20
|
+
options.prefix = prefix
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveLdap::Base.setup_connection
|
25
|
+
config = ActiveLdap::Base.configuration
|
26
|
+
|
27
|
+
LDAP_PREFIX = options.prefix
|
28
|
+
LDAP_USER = config[:bind_dn]
|
29
|
+
LDAP_PASSWORD = config[:password]
|
30
|
+
|
31
|
+
N_USERS = 100
|
32
|
+
|
33
|
+
class ALUser < ActiveLdap::Base
|
34
|
+
ldap_mapping :dn_attribute => 'uid', :prefix => LDAP_PREFIX,
|
35
|
+
:classes => ['posixAccount', 'person']
|
36
|
+
end
|
37
|
+
|
38
|
+
def populate_base
|
39
|
+
ActiveLdap::Populate.ensure_base
|
40
|
+
if ActiveLdap::Base.search.empty?
|
41
|
+
raise "Can't populate #{ActiveLdap::Base.base}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def populate_users
|
46
|
+
ou_class = Class.new(ActiveLdap::Base)
|
47
|
+
ou_class.ldap_mapping :dn_attribute => "ou",
|
48
|
+
:prefix => "",
|
49
|
+
:classes => ["top", "organizationalUnit"]
|
50
|
+
ou_class.new(LDAP_PREFIX.split(/=/)[1]).save!
|
51
|
+
|
52
|
+
N_USERS.times do |i|
|
53
|
+
name = i.to_s
|
54
|
+
user = ALUser.new(name)
|
55
|
+
user.uid_number = 100000 + i
|
56
|
+
user.gid_number = 100000 + i
|
57
|
+
user.cn = name
|
58
|
+
user.sn = name
|
59
|
+
user.home_directory = "/nonexistent"
|
60
|
+
user.save!
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def populate
|
65
|
+
populate_base
|
66
|
+
populate_users
|
67
|
+
end
|
68
|
+
|
69
|
+
def main(do_populate)
|
70
|
+
if do_populate
|
71
|
+
puts(_("Populating..."))
|
72
|
+
dumped_data = ActiveLdap::Base.dump(:scope => :sub)
|
73
|
+
ActiveLdap::Base.delete_all(nil, :scope => :sub)
|
74
|
+
populate
|
75
|
+
puts
|
76
|
+
end
|
77
|
+
|
78
|
+
Benchmark.bmbm(20) do |x|
|
79
|
+
n = 100
|
80
|
+
GC.start
|
81
|
+
x.report("search 100 entries") do
|
82
|
+
n.times {ALUser.search}
|
83
|
+
end
|
84
|
+
GC.start
|
85
|
+
x.report("instantiate 1 entry") do
|
86
|
+
n.times {ALUser.first}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
ensure
|
90
|
+
if do_populate
|
91
|
+
puts
|
92
|
+
puts(_("Cleaning..."))
|
93
|
+
ActiveLdap::Base.delete_all(nil, :scope => :sub)
|
94
|
+
ActiveLdap::Base.load(dumped_data)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
main(LDAP_USER && LDAP_PASSWORD)
|
data/doc/text/news.textile
CHANGED
@@ -1,5 +1,43 @@
|
|
1
1
|
h1. News
|
2
2
|
|
3
|
+
h2(#3-2-0). 3.2.0: 2012-08-29
|
4
|
+
|
5
|
+
* [GitHub:#39] Supported Rails 3.2.8. [Reported by Ben Langfeld]
|
6
|
+
* [GitHub:#13] Don't use deprecated Gem.available?. [Patch by sailesh]
|
7
|
+
* [GitHub:#19] Supported new entry by @ha_many :wrap@. [Patch by Alex Tomlins]
|
8
|
+
* Supported @:only@ option in XML output.
|
9
|
+
* [GitHub:#14] Supported nil as single value. [Reported by n3llyb0y]
|
10
|
+
* [GitHub:#20] Supported ActiveModel::MassAssignmentSecurity.
|
11
|
+
[Reported by mihu]
|
12
|
+
* [GitHub:#24] Supported Ruby 1.9 style Hash syntax in generator.
|
13
|
+
[Patch by ursm]
|
14
|
+
* [GitHub:#25][GitHub:#39] Supported ActiveModel::Dirty.
|
15
|
+
[Patch by mihu][Reported by Ben Langfeld]
|
16
|
+
* [GitHub:#26] Improved speed for dirty. [Patch by mihu]
|
17
|
+
* [GitHub:#28] Improved speed for initialization. [Patch by mihu]
|
18
|
+
* [GitHub:#29] Added .gemspec. [Suggested by mklappstuhl]
|
19
|
+
* [GitHub:#34] Removed an unused method. [Patch by mihu]
|
20
|
+
* [GitHub:#37] Improved will_paginate support. [Patch by Craig White]
|
21
|
+
* [GitHub:#40] Added missing test files to .gemspec. [Reported by Vít Ondruch]
|
22
|
+
* [GitHub:#41] Improved speed for find. [Patch by unixmechanic]
|
23
|
+
* Changed i18n backend to gettext from fast_gettext again.
|
24
|
+
* [GitHub:#42] Fixed a bug that optional second is required for GeneralizedTime.
|
25
|
+
[Reported by masche842]
|
26
|
+
|
27
|
+
h3. Thanks
|
28
|
+
|
29
|
+
* sailesh
|
30
|
+
* Alex Tomlins
|
31
|
+
* n3llyb0y
|
32
|
+
* mihu
|
33
|
+
* ursm
|
34
|
+
* Ben Langfeld
|
35
|
+
* mklappstuhl
|
36
|
+
* Craig White
|
37
|
+
* Vít Ondruch
|
38
|
+
* unixmechanic
|
39
|
+
* masche842
|
40
|
+
|
3
41
|
h2(#3-1-1). 3.1.1: 2011-11-03
|
4
42
|
|
5
43
|
* Supported Rails 3.1.1.
|
data/lib/active_ldap.rb
CHANGED
@@ -2,8 +2,9 @@ require "rubygems"
|
|
2
2
|
require "active_model"
|
3
3
|
require "active_support/core_ext"
|
4
4
|
|
5
|
+
require "active_ldap/version"
|
6
|
+
|
5
7
|
module ActiveLdap
|
6
|
-
VERSION = "3.1.1"
|
7
8
|
autoload :Command, "active_ldap/command"
|
8
9
|
end
|
9
10
|
|
@@ -13,11 +14,6 @@ else
|
|
13
14
|
require 'active_ldap/timeout_stub'
|
14
15
|
end
|
15
16
|
|
16
|
-
begin
|
17
|
-
require "locale"
|
18
|
-
require "fast_gettext"
|
19
|
-
rescue LoadError
|
20
|
-
end
|
21
17
|
require 'active_ldap/get_text'
|
22
18
|
|
23
19
|
require 'active_ldap/compatible'
|
@@ -32,6 +28,12 @@ require 'active_ldap/persistence'
|
|
32
28
|
|
33
29
|
require 'active_ldap/associations'
|
34
30
|
require 'active_ldap/attributes'
|
31
|
+
require 'active_ldap/attribute_methods'
|
32
|
+
require 'active_ldap/attribute_methods/query'
|
33
|
+
require 'active_ldap/attribute_methods/before_type_cast'
|
34
|
+
require 'active_ldap/attribute_methods/read'
|
35
|
+
require 'active_ldap/attribute_methods/write'
|
36
|
+
require 'active_ldap/attribute_methods/dirty'
|
35
37
|
require 'active_ldap/configuration'
|
36
38
|
require 'active_ldap/connection'
|
37
39
|
require 'active_ldap/operations'
|
@@ -50,15 +52,22 @@ require 'active_ldap/callbacks'
|
|
50
52
|
|
51
53
|
|
52
54
|
ActiveLdap::Base.class_eval do
|
55
|
+
include ActiveLdap::Persistence
|
56
|
+
|
53
57
|
include ActiveLdap::Associations
|
58
|
+
include ActiveModel::MassAssignmentSecurity
|
54
59
|
include ActiveLdap::Attributes
|
60
|
+
include ActiveLdap::AttributeMethods
|
61
|
+
include ActiveLdap::AttributeMethods::BeforeTypeCast
|
62
|
+
include ActiveLdap::AttributeMethods::Write
|
63
|
+
include ActiveLdap::AttributeMethods::Dirty
|
64
|
+
include ActiveLdap::AttributeMethods::Query
|
65
|
+
include ActiveLdap::AttributeMethods::Read
|
55
66
|
include ActiveLdap::Configuration
|
56
67
|
include ActiveLdap::Connection
|
57
68
|
include ActiveLdap::Operations
|
58
69
|
include ActiveLdap::ObjectClass
|
59
70
|
|
60
|
-
include ActiveLdap::Persistence
|
61
|
-
|
62
71
|
include ActiveLdap::Acts::Tree
|
63
72
|
|
64
73
|
include ActiveLdap::Validations
|
@@ -18,7 +18,7 @@ module ActiveLdap
|
|
18
18
|
new_value = (old_value + current_value).uniq.sort
|
19
19
|
if old_value != new_value
|
20
20
|
@owner[@options[:wrap]] = new_value
|
21
|
-
@owner.save
|
21
|
+
@owner.save unless @owner.new_entry?
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -34,7 +34,7 @@ module ActiveLdap
|
|
34
34
|
new_value = new_value.uniq.sort
|
35
35
|
if old_value != new_value
|
36
36
|
@owner[@options[:wrap]] = new_value
|
37
|
-
@owner.save
|
37
|
+
@owner.save unless @owner.new_entry?
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -57,6 +57,19 @@ module ActiveLdap
|
|
57
57
|
def foreign_key
|
58
58
|
@options[:primary_key_name] || foreign_class.dn_attribute
|
59
59
|
end
|
60
|
+
|
61
|
+
def add_entries(*entries)
|
62
|
+
result = true
|
63
|
+
load_target
|
64
|
+
|
65
|
+
flatten_deeper(entries).each do |entry|
|
66
|
+
infect_connection(entry)
|
67
|
+
insert_entry(entry) or result = false
|
68
|
+
@target << entry
|
69
|
+
end
|
70
|
+
|
71
|
+
result && self
|
72
|
+
end
|
60
73
|
end
|
61
74
|
end
|
62
75
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveLdap
|
2
|
+
module AttributeMethods
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ActiveModel::AttributeMethods
|
5
|
+
|
6
|
+
def methods(singleton_methods = true)
|
7
|
+
target_names = entry_attribute.all_names
|
8
|
+
target_names -= ['objectClass', 'objectClass'.underscore]
|
9
|
+
super + target_names.uniq.collect do |attr|
|
10
|
+
self.class.attribute_method_matchers.collect do |matcher|
|
11
|
+
:"#{matcher.prefix}#{attr}#{matcher.suffix}"
|
12
|
+
end
|
13
|
+
end.flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# overiding ActiveModel::AttributeMethods
|
19
|
+
def attribute_method?(method_name)
|
20
|
+
have_attribute?(method_name, ['objectClass'])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveLdap
|
2
|
+
module AttributeMethods
|
3
|
+
module BeforeTypeCast
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attribute_method_suffix '_before_type_cast'
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
def attribute_before_type_cast(attr)
|
12
|
+
get_attribute_before_type_cast(attr)[1]
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_attribute_before_type_cast(name, force_array=false)
|
16
|
+
name = to_real_attribute_name(name)
|
17
|
+
|
18
|
+
value = @data[name]
|
19
|
+
value = [] if value.nil?
|
20
|
+
[name, array_of(value, force_array)]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
module ActiveLdap
|
3
|
+
module AttributeMethods
|
4
|
+
module Dirty
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ActiveModel::Dirty
|
7
|
+
|
8
|
+
# Attempts to +save+ the record and clears changed attributes if successful.
|
9
|
+
def save(*) #:nodoc:
|
10
|
+
succeeded = super
|
11
|
+
if succeeded
|
12
|
+
@previously_changed = changes
|
13
|
+
@changed_attributes.clear
|
14
|
+
end
|
15
|
+
succeeded
|
16
|
+
end
|
17
|
+
|
18
|
+
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
19
|
+
def save!(*) #:nodoc:
|
20
|
+
super.tap do
|
21
|
+
@previously_changed = changes
|
22
|
+
@changed_attributes.clear
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# <tt>reload</tt> the record and clears changed attributes.
|
27
|
+
def reload(*) #:nodoc:
|
28
|
+
super.tap do
|
29
|
+
@previously_changed.clear
|
30
|
+
@changed_attributes.clear
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
def set_attribute(name, value)
|
36
|
+
if name != "objectClass"
|
37
|
+
attribute_will_change!(name) unless value == get_attribute(name)
|
38
|
+
end
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|