kpm 0.0.15 → 0.1.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.
@@ -1,133 +1,99 @@
1
1
  require 'logger'
2
+ require 'pathname'
2
3
  require 'yaml'
3
4
 
4
5
  module KPM
5
- class Installer
6
- LATEST_VERSION = 'LATEST'
7
- SHA1_FILENAME = 'sha1.yml'
6
+ class Installer < BaseInstaller
8
7
 
9
- def self.from_file(config_path, logger=nil)
10
- Installer.new(YAML::load_file(config_path), logger)
8
+ def self.from_file(config_path=nil, logger=nil)
9
+ if config_path.nil?
10
+ # Install Kill Bill, Kaui and the KPM plugin by default
11
+ config = {'killbill' => {'version' => 'LATEST', 'plugins' => {'ruby' => [{'name' => 'kpm'}]}}, 'kaui' => {'version' => 'LATEST'}}
12
+ else
13
+ config = YAML::load_file(config_path)
14
+ end
15
+ Installer.new(config, logger)
11
16
  end
12
17
 
13
18
  def initialize(raw_config, logger=nil)
14
-
15
- raise(ArgumentError, 'killbill or kaui section must be specified') if raw_config['killbill'].nil? and raw_config['kaui'].nil?
16
- @config = raw_config['killbill']
19
+ @config = raw_config['killbill']
17
20
  @kaui_config = raw_config['kaui']
18
21
 
19
22
  if logger.nil?
20
- @logger = Logger.new(STDOUT)
21
- @logger.level = Logger::INFO
22
- else
23
- @logger = logger
23
+ logger = Logger.new(STDOUT)
24
+ logger.level = Logger::INFO
24
25
  end
25
26
 
26
- @nexus_config = !@config.nil? ? @config['nexus'] : @kaui_config['nexus']
27
- @nexus_ssl_verify = !@nexus_config.nil? ? @nexus_config['ssl_verify'] : true
27
+ nexus_config = !@config.nil? ? @config['nexus'] : (!@kaui_config.nil? ? @kaui_config['nexus'] : nil)
28
+ nexus_ssl_verify = !nexus_config.nil? ? nexus_config['ssl_verify'] : true
29
+
30
+ super(logger, nexus_config, nexus_ssl_verify)
28
31
  end
29
32
 
30
33
  def install(force_download=false, verify_sha1=true)
31
- @force_download = force_download
32
- @verify_sha1 = verify_sha1
33
-
34
+ help = nil
34
35
  unless @config.nil?
35
- @bundles_dir = @config['plugins_dir']
36
- @sha1_file = "#{@bundles_dir}/#{SHA1_FILENAME}"
37
-
38
- install_killbill_server
39
- install_plugins
40
- install_default_bundles
36
+ help = install_tomcat if @config['webapp_path'].nil?
37
+ install_killbill_server(@config['group_id'], @config['artifact_id'], @config['packaging'], @config['classifier'], @config['version'], @config['webapp_path'], force_download, verify_sha1)
38
+ install_plugins(force_download, verify_sha1)
39
+ unless @config['default_bundles'] == false
40
+ install_default_bundles(@config['plugins_dir'], @config['default_bundles_version'], @config['version'], force_download, verify_sha1)
41
+ end
41
42
  end
43
+
42
44
  unless @kaui_config.nil?
43
- install_kaui
45
+ if @kaui_config['webapp_path'].nil?
46
+ @logger.warn('No webapp_path specified for Kaui, aborting installation')
47
+ return
48
+ end
49
+
50
+ install_kaui(@kaui_config['group_id'], @kaui_config['artifact_id'], @kaui_config['packaging'], @kaui_config['classifier'], @kaui_config['version'], @kaui_config['webapp_path'], force_download, verify_sha1)
44
51
  end
52
+
53
+ help
45
54
  end
46
55
 
47
56
  private
48
57
 
49
- def install_killbill_server
50
- group_id = @config['group_id'] || KPM::BaseArtifact::KILLBILL_GROUP_ID
51
- artifact_id = @config['artifact_id'] || KPM::BaseArtifact::KILLBILL_ARTIFACT_ID
52
- packaging = @config['packaging'] || KPM::BaseArtifact::KILLBILL_PACKAGING
53
- classifier = @config['classifier'] || KPM::BaseArtifact::KILLBILL_CLASSIFIER
54
- version = @config['version'] || LATEST_VERSION
55
- webapp_path = @config['webapp_path'] || KPM::root
58
+ def install_tomcat(dir=Dir.pwd)
59
+ # Download and unpack Tomcat
60
+ manager = KPM::TomcatManager.new(dir, @logger)
61
+ manager.download
62
+
63
+ # Update main config
64
+ root_war_path = manager.setup
65
+ @config['webapp_path'] = root_war_path
66
+ @kaui_config['webapp_path'] = Pathname.new(File.dirname(root_war_path)).join('kaui.war').to_s
56
67
 
57
- KPM::KillbillServerArtifact.pull(@logger, group_id, artifact_id, packaging, classifier, version, webapp_path, nil, @force_download, @verify_sha1, @nexus_config, @nexus_ssl_verify)
68
+ # Help message
69
+ manager.help
58
70
  end
59
71
 
60
- def install_plugins
61
- install_java_plugins
62
- install_ruby_plugins
72
+ def install_plugins(force_download, verify_sha1)
73
+ install_java_plugins(force_download, verify_sha1)
74
+ install_ruby_plugins(force_download, verify_sha1)
63
75
  end
64
76
 
65
- def install_java_plugins
77
+ def install_java_plugins(force_download, verify_sha1)
66
78
  return if @config['plugins'].nil? or @config['plugins']['java'].nil?
67
79
 
68
80
  infos = []
69
81
  @config['plugins']['java'].each do |plugin|
70
- group_id = plugin['group_id'] || KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_GROUP_ID
71
- artifact_id = plugin['artifact_id'] || plugin['name']
72
- packaging = plugin['packaging'] || KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_PACKAGING
73
- classifier = plugin['classifier'] || KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_CLASSIFIER
74
- version = plugin['version'] || LATEST_VERSION
75
- destination = "#{@bundles_dir}/plugins/java/#{artifact_id}/#{version}"
76
-
77
- infos << KPM::KillbillPluginArtifact.pull(@logger, group_id, artifact_id, packaging, classifier, version, destination, @sha1_file, @force_download, @verify_sha1, @nexus_config, @nexus_ssl_verify)
82
+ infos << install_plugin(plugin['group_id'], plugin['artifact_id'] || plugin['name'], plugin['packaging'], plugin['classifier'], plugin['version'], @config['plugins_dir'], 'java', force_download, verify_sha1)
78
83
  end
79
84
 
80
85
  infos
81
86
  end
82
87
 
83
- def install_ruby_plugins
88
+ def install_ruby_plugins(force_download, verify_sha1)
84
89
  return if @config['plugins'].nil? or @config['plugins']['ruby'].nil?
85
90
 
86
91
  infos = []
87
92
  @config['plugins']['ruby'].each do |plugin|
88
- group_id = plugin['group_id'] || KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_GROUP_ID
89
- artifact_id = plugin['artifact_id'] || plugin['name']
90
- packaging = plugin['packaging'] || KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_PACKAGING
91
- classifier = plugin['classifier'] || KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_CLASSIFIER
92
- version = plugin['version'] || LATEST_VERSION
93
- destination = "#{@bundles_dir}/plugins/ruby"
94
-
95
- infos << KPM::KillbillPluginArtifact.pull(@logger, group_id, artifact_id, packaging, classifier, version, destination, @sha1_file, @force_download, @verify_sha1, @nexus_config, @nexus_ssl_verify)
93
+ infos << install_plugin(plugin['group_id'], plugin['artifact_id'] || plugin['name'], plugin['packaging'], plugin['classifier'], plugin['version'], @config['plugins_dir'], 'ruby', force_download, verify_sha1)
96
94
  end
97
95
 
98
96
  infos
99
97
  end
100
-
101
- def install_default_bundles
102
- return if @config['default_bundles'] == false
103
-
104
- group_id = 'org.kill-bill.billing'
105
- artifact_id = 'killbill-platform-osgi-bundles-defaultbundles'
106
- packaging = 'tar.gz'
107
- classifier = nil
108
- version = @config['default_bundles_version'] || LATEST_VERSION
109
- destination = "#{@config['plugins_dir']}/platform"
110
-
111
- info = KPM::BaseArtifact.pull(@logger, group_id, artifact_id, packaging, classifier, version, destination, @sha1_file, @force_download, @verify_sha1, @nexus_config, @nexus_ssl_verify)
112
-
113
- # The special JRuby bundle needs to be called jruby.jar
114
- # TODO .first - code smell
115
- if !info[:skipped]
116
- File.rename Dir.glob("#{destination}/killbill-platform-osgi-bundles-jruby-*.jar").first, "#{destination}/jruby.jar"
117
- end
118
-
119
- info
120
- end
121
-
122
- def install_kaui
123
- group_id = @kaui_config['group_id'] || KPM::BaseArtifact::KAUI_GROUP_ID
124
- artifact_id = @kaui_config['artifact_id'] || KPM::BaseArtifact::KAUI_ARTIFACT_ID
125
- packaging = @kaui_config['packaging'] || KPM::BaseArtifact::KAUI_PACKAGING
126
- classifier = @kaui_config['classifier'] || KPM::BaseArtifact::KAUI_CLASSIFIER
127
- version = @kaui_config['version'] || LATEST_VERSION
128
- webapp_path = @kaui_config['webapp_path'] || KPM::root
129
-
130
- KPM::KauiArtifact.pull(@logger, group_id, artifact_id, packaging, classifier, version, webapp_path, nil, @force_download, @verify_sha1, @nexus_config, @nexus_ssl_verify)
131
- end
132
98
  end
133
99
  end
@@ -11,6 +11,57 @@ module KPM
11
11
  response.elements.each('search-results/data/artifact/version') { |element| versions << element.text }
12
12
  versions
13
13
  end
14
+
15
+ def info(version='LATEST', overrides={}, ssl_verify=true)
16
+ logger = Logger.new(STDOUT)
17
+ logger.level = Logger::ERROR
18
+
19
+ versions = {}
20
+ Dir.mktmpdir do |dir|
21
+ # Retrieve the main Kill Bill pom
22
+ kb_pom_info = pull(logger,
23
+ KPM::BaseArtifact::KILLBILL_GROUP_ID,
24
+ 'killbill',
25
+ 'pom',
26
+ nil,
27
+ version,
28
+ dir,
29
+ nil,
30
+ false,
31
+ true,
32
+ overrides,
33
+ ssl_verify)
34
+
35
+ # Extract the killbill-oss-parent version
36
+ pom = REXML::Document.new(File.new(kb_pom_info[:file_path]))
37
+ oss_parent_version = pom.root.elements['parent/version'].text
38
+ kb_version = pom.root.elements['version'].text
39
+
40
+ versions['killbill'] = kb_version
41
+ versions['killbill-oss-parent'] = oss_parent_version
42
+
43
+ # Retrieve the killbill-oss-parent pom
44
+ oss_pom_info = pull(logger,
45
+ KPM::BaseArtifact::KILLBILL_GROUP_ID,
46
+ 'killbill-oss-parent',
47
+ 'pom',
48
+ nil,
49
+ oss_parent_version,
50
+ dir,
51
+ nil,
52
+ false,
53
+ true,
54
+ overrides,
55
+ ssl_verify)
56
+
57
+ pom = REXML::Document.new(File.new(oss_pom_info[:file_path]))
58
+ properties_element = pom.root.elements['properties']
59
+ %w(killbill-api killbill-plugin-api killbill-commons killbill-platform).each do |property|
60
+ versions[property] = properties_element.elements["#{property}.version"].text
61
+ end
62
+ end
63
+ versions
64
+ end
14
65
  end
15
66
  end
16
67
  end
@@ -14,20 +14,34 @@ module KPM
14
14
  end
15
15
  end
16
16
 
17
- def self.lookup(plugin_name, latest=false)
18
- plugin = all(latest)[plugin_name.to_s.downcase.to_sym]
17
+ # Note: this API is used in Docker images (see kpm_generator.rb, careful when changing it!)
18
+ def self.lookup(raw_plugin_name, latest=false, raw_kb_version=nil)
19
+ plugin_name = raw_plugin_name.to_s.downcase
20
+ plugin = all(latest)[plugin_name.to_sym]
19
21
  return nil if plugin.nil?
20
22
 
21
23
  type = plugin[:type]
22
24
  is_ruby = type == :ruby
23
25
 
24
26
  group_id = plugin[:group_id] || (is_ruby ? KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_GROUP_ID : KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_GROUP_ID)
25
- artifact_id = plugin[:artifact_id] || "#{plugin.to_s}-plugin"
27
+ artifact_id = plugin[:artifact_id] || "#{plugin_name}-plugin"
26
28
  packaging = plugin[:packaging] || (is_ruby ? KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_PACKAGING : KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_PACKAGING)
27
29
  classifier = plugin[:classifier] || (is_ruby ? KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_CLASSIFIER : KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_CLASSIFIER)
28
- version = plugin[:stable_version] || 'LATEST'
30
+
31
+ if raw_kb_version == 'LATEST'
32
+ version = 'LATEST'
33
+ else
34
+ # Keep supporting the deprecated key :stable_version for now
35
+ captures = raw_kb_version.nil? ? [] : raw_kb_version.scan(/(\d+\.\d+)(\.\d)?/)
36
+ if captures.empty? || captures.first.nil? || captures.first.first.nil?
37
+ version = plugin[:stable_version] || 'LATEST'
38
+ else
39
+ kb_version = captures.first.first
40
+ version = (plugin[:versions] || {})[kb_version.to_sym] || 'LATEST'
41
+ end
42
+ end
29
43
 
30
44
  [group_id, artifact_id, packaging, classifier, version, type]
31
45
  end
32
46
  end
33
- end
47
+ end
@@ -1,42 +1,146 @@
1
1
  ---
2
+ :accertify:
3
+ :type: :java
4
+ :versions:
5
+ :0.14: 0.1.0
6
+ :require:
7
+ - :org.killbill.billing.plugin.accertify.url
8
+ - :org.killbill.billing.plugin.accertify.username
9
+ - :org.killbill.billing.plugin.accertify.password
10
+ :adyen:
11
+ :type: :java
12
+ :versions:
13
+ :0.14: 0.1.0
14
+ :0.15: 0.2.1
15
+ :require:
16
+ - :org.killbill.billing.plugin.adyen.merchantAccount
17
+ - :org.killbill.billing.plugin.adyen.username
18
+ - :org.killbill.billing.plugin.adyen.password
19
+ - :org.killbill.billing.plugin.adyen.paymentUrl
20
+ :analytics:
21
+ :type: :java
22
+ :versions:
23
+ :0.14: 1.0.3
24
+ :0.15: 2.0.1
25
+ :stable_version: 2.0.2
26
+ :avatax:
27
+ :type: :java
28
+ :versions:
29
+ :0.14: 0.1.0
30
+ :0.15: 0.2.0
31
+ :stable_version: 0.2.0
32
+ :require:
33
+ - :org.killbill.billing.plugin.avatax.url
34
+ - :org.killbill.billing.plugin.avatax.accountNumber
35
+ - :org.killbill.billing.plugin.avatax.licenseKey
2
36
  :bitpay:
3
37
  :type: :ruby
4
- :artifact_id: bitpay-plugin
38
+ :versions:
39
+ :0.14: 0.0.1
5
40
  :stable_version: 0.0.1
41
+ :require:
42
+ - :api_key
43
+ :braintree_blue:
44
+ :type: :ruby
45
+ :versions:
46
+ :0.14: 0.0.1
47
+ :stable_version: 0.1.0
48
+ :require:
49
+ - :merchant_id
50
+ - :public_key
51
+ - :private_key
6
52
  :coinbase:
7
53
  :type: :ruby
8
- :artifact_id: coinbase-plugin
54
+ :versions:
9
55
  :stable_version: 0.0.1
56
+ :require:
57
+ - :btc_address
58
+ - :api_key
59
+ :currency:
60
+ :type: :ruby
61
+ :artifact_id: killbill-currency-plugin
62
+ :versions:
10
63
  :cybersource:
11
64
  :type: :ruby
12
- :artifact_id: cybersource-plugin
65
+ :versions:
66
+ :0.14: 1.0.0
67
+ :0.15: 3.0.0
13
68
  :stable_version: 0.0.4
14
69
  :require:
15
- - :login
16
- - :password
70
+ - :login
71
+ - :password
72
+ :email-notifications:
73
+ :type: :java
74
+ :artifact_id: killbill-email-notifications-plugin
75
+ :versions:
76
+ :0.14: 0.1.0
77
+ :stable_version: 0.1.0
78
+ :forte:
79
+ :type: :java
80
+ :versions:
81
+ :0.14: 0.1.0
82
+ :require:
83
+ - :org.killbill.billing.plugin.forte.merchantId
84
+ - :org.killbill.billing.plugin.forte.password
85
+ - :org.killbill.billing.plugin.forte.host
86
+ - :org.killbill.billing.plugin.forte.port
87
+ - :org.killbill.billing.plugin.forte.apiLoginId
88
+ - :org.killbill.billing.plugin.forte.secureTransactionKey
89
+ :kpm:
90
+ :type: :ruby
91
+ :versions:
92
+ :0.15: 0.0.1
17
93
  :litle:
18
94
  :type: :ruby
19
- :artifact_id: litle-plugin
95
+ :versions:
96
+ :0.14: 2.0.0
20
97
  :stable_version: 1.10.0
98
+ :require:
99
+ - :account_id
100
+ - :merchant_id
101
+ - :username
102
+ - :password
103
+ - :secure_page_url
104
+ - :paypage_id
105
+ :logging:
106
+ :type: :ruby
107
+ :versions:
108
+ :0.14: 1.7.0
109
+ :0.15: 2.0.0
21
110
  :paypal:
22
111
  :type: :ruby
23
112
  :artifact_id: paypal-express-plugin
24
- :stable_version: 1.8.1
113
+ :versions:
114
+ :0.14: 2.0.0
115
+ :0.15: 3.0.0
116
+ :stable_version: 2.0.0
25
117
  :require:
26
118
  - :signature
27
119
  - :login
28
120
  - :password
121
+ :payu_latam:
122
+ :type: :ruby
123
+ :artifact_id: payu-latam-plugin
124
+ :versions:
125
+ :0.14: 0.1.0
126
+ :require:
127
+ - :api_login
128
+ - :api_key
129
+ - :country_account_id
130
+ - :merchant_id
29
131
  :stripe:
30
132
  :type: :ruby
31
- :artifact_id: stripe-plugin
32
- :stable_version: 0.2.1
133
+ :versions:
134
+ :0.14: 1.0.0
135
+ :0.15: 2.0.0
136
+ :stable_version: 2.0.0
33
137
  :require:
34
138
  - :api_secret_key
35
- :braintree_blue:
139
+ :zendesk:
36
140
  :type: :ruby
37
- :artifact_id: braintree_blue-plugin
38
- :stable_version: 0.0.1
141
+ :versions:
142
+ :0.14: 1.3.0
39
143
  :require:
40
- - :merchant_id
41
- - :public_key
42
- - :private_key
144
+ - :subdomain
145
+ - :username
146
+ - :password
@@ -0,0 +1,110 @@
1
+ require 'pathname'
2
+
3
+ module KPM
4
+ class PluginsManager
5
+
6
+ def initialize(plugins_dir, logger)
7
+ @plugins_dir = Pathname.new(plugins_dir)
8
+ @logger = logger
9
+ end
10
+
11
+ def set_active(plugin_name_or_path, plugin_version=nil)
12
+ if plugin_name_or_path.nil?
13
+ @logger.warn('Unable to mark a plugin as active: no name or path specified')
14
+ return
15
+ end
16
+
17
+ if plugin_version.nil?
18
+ # Full path specified, with version
19
+ link = Pathname.new(plugin_name_or_path).join('../ACTIVE')
20
+ FileUtils.rm_f(link)
21
+ FileUtils.ln_s(plugin_name_or_path, link, :force => true)
22
+ else
23
+ # Plugin name (fs directory) specified
24
+ plugin_dir_glob = @plugins_dir.join('*').join(plugin_name_or_path)
25
+ # Only one should match (java or ruby plugin)
26
+ Dir.glob(plugin_dir_glob).each do |plugin_dir_path|
27
+ plugin_dir = Pathname.new(plugin_dir_path)
28
+ link = plugin_dir.join('ACTIVE')
29
+ FileUtils.rm_f(link)
30
+ FileUtils.ln_s(plugin_dir.join(plugin_version), link, :force => true)
31
+ end
32
+ end
33
+
34
+ update_fs(plugin_name_or_path, plugin_version) do |tmp_dir|
35
+ FileUtils.rm_f(tmp_dir.join('stop.txt'))
36
+ FileUtils.rm_f(tmp_dir.join('restart.txt'))
37
+ end
38
+ end
39
+
40
+ def uninstall(plugin_name_or_path, plugin_version=nil)
41
+ update_fs(plugin_name_or_path, plugin_version) do |tmp_dir|
42
+ FileUtils.rm_f(tmp_dir.join('restart.txt'))
43
+ # Be safe, keep the code, just never start it
44
+ FileUtils.touch(tmp_dir.join('stop.txt'))
45
+ end
46
+ end
47
+
48
+ def restart(plugin_name_or_path, plugin_version=nil)
49
+ update_fs(plugin_name_or_path, plugin_version) do |tmp_dir|
50
+ # Remove stop.txt so that the plugin is started if it was stopped
51
+ FileUtils.rm_f(tmp_dir.join('stop.txt'))
52
+ FileUtils.touch(tmp_dir.join('restart.txt'))
53
+ end
54
+ end
55
+
56
+ def guess_plugin_name(artifact_id)
57
+ return nil if artifact_id.nil?
58
+ captures = artifact_id.scan(/(.*)-plugin/)
59
+ if captures.empty? || captures.first.nil? || captures.first.first.nil?
60
+ short_name = artifact_id
61
+ else
62
+ # 'analytics-plugin' or 'stripe-plugin' passed
63
+ short_name = captures.first.first
64
+ end
65
+ Dir.glob(@plugins_dir.join('*').join('*')).each do |plugin_path|
66
+ plugin_name = File.basename(plugin_path)
67
+ if plugin_name == short_name ||
68
+ plugin_name == artifact_id ||
69
+ !plugin_name.scan(/-#{short_name}/).empty? ||
70
+ !plugin_name.scan(/#{short_name}-/).empty?
71
+ return plugin_name
72
+ end
73
+ end
74
+ nil
75
+ end
76
+
77
+ private
78
+
79
+ # Note: the plugin name here is the directory name on the filesystem
80
+ def update_fs(plugin_name_or_path, plugin_version=nil, &block)
81
+ if plugin_name_or_path.nil?
82
+ @logger.warn('Unable to update the filesystem: no name or path specified')
83
+ return
84
+ end
85
+
86
+ p = plugin_version.nil? ? plugin_name_or_path : @plugins_dir.join('*').join(plugin_name_or_path).join(plugin_version == :all ? '*' : plugin_version)
87
+
88
+ modified = []
89
+ Dir.glob(p).each do |plugin_dir_path|
90
+ plugin_dir = Pathname.new(plugin_dir_path)
91
+ tmp_dir = plugin_dir.join('tmp')
92
+ FileUtils.mkdir_p(tmp_dir)
93
+
94
+ yield(tmp_dir) if block_given?
95
+
96
+ modified << plugin_dir
97
+ end
98
+
99
+ if modified.empty?
100
+ if plugin_version.nil?
101
+ @logger.warn("No plugin found with name #{plugin_name_or_path}. Installed plugins: #{Dir.glob(@plugins_dir.join('*').join('*'))}")
102
+ else
103
+ @logger.warn("No plugin found with name #{plugin_name_or_path} and version #{plugin_version}. Installed plugins: #{Dir.glob(@plugins_dir.join('*').join('*').join('*'))}")
104
+ end
105
+ end
106
+
107
+ modified
108
+ end
109
+ end
110
+ end