octocore-cassandra 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.md +12 -0
  3. data/CONTRIBUTING +0 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +202 -0
  6. data/MAINTAINERS +0 -0
  7. data/NOTICE +8 -0
  8. data/README.md +69 -0
  9. data/Rakefile +142 -0
  10. data/bin/fakestream-cassandra +258 -0
  11. data/bin/octocore-admin-cassandra +54 -0
  12. data/lib/octocore-cassandra.rb +152 -0
  13. data/lib/octocore-cassandra/baseline.rb +131 -0
  14. data/lib/octocore-cassandra/callbacks.rb +117 -0
  15. data/lib/octocore-cassandra/config.rb +39 -0
  16. data/lib/octocore-cassandra/config/config.yml +1 -0
  17. data/lib/octocore-cassandra/config/search/index/user.yml +42 -0
  18. data/lib/octocore-cassandra/counter.rb +265 -0
  19. data/lib/octocore-cassandra/counter/helpers.rb +168 -0
  20. data/lib/octocore-cassandra/email.rb +63 -0
  21. data/lib/octocore-cassandra/featureflag.rb +79 -0
  22. data/lib/octocore-cassandra/helpers.rb +6 -0
  23. data/lib/octocore-cassandra/helpers/api_consumer_helper.rb +51 -0
  24. data/lib/octocore-cassandra/helpers/api_helper.rb +65 -0
  25. data/lib/octocore-cassandra/helpers/api_logger.rb +14 -0
  26. data/lib/octocore-cassandra/helpers/client_helper.rb +104 -0
  27. data/lib/octocore-cassandra/helpers/kong_helper.rb +164 -0
  28. data/lib/octocore-cassandra/helpers/sinatra_helper.rb +22 -0
  29. data/lib/octocore-cassandra/kafka_bridge.rb +60 -0
  30. data/lib/octocore-cassandra/kldivergence.rb +14 -0
  31. data/lib/octocore-cassandra/mailer.rb +1 -0
  32. data/lib/octocore-cassandra/mailer/subscriber_mailer.rb +30 -0
  33. data/lib/octocore-cassandra/message_parser.rb +114 -0
  34. data/lib/octocore-cassandra/models.rb +274 -0
  35. data/lib/octocore-cassandra/models/contactus.rb +42 -0
  36. data/lib/octocore-cassandra/models/enterprise.rb +76 -0
  37. data/lib/octocore-cassandra/models/enterprise/adapter_details.rb +18 -0
  38. data/lib/octocore-cassandra/models/enterprise/api_event.rb +14 -0
  39. data/lib/octocore-cassandra/models/enterprise/api_hit.rb +20 -0
  40. data/lib/octocore-cassandra/models/enterprise/api_key.rb +11 -0
  41. data/lib/octocore-cassandra/models/enterprise/api_track.rb +13 -0
  42. data/lib/octocore-cassandra/models/enterprise/app_init.rb +13 -0
  43. data/lib/octocore-cassandra/models/enterprise/app_login.rb +12 -0
  44. data/lib/octocore-cassandra/models/enterprise/app_logout.rb +12 -0
  45. data/lib/octocore-cassandra/models/enterprise/authorization.rb +67 -0
  46. data/lib/octocore-cassandra/models/enterprise/category.rb +14 -0
  47. data/lib/octocore-cassandra/models/enterprise/category_baseline.rb +19 -0
  48. data/lib/octocore-cassandra/models/enterprise/category_hit.rb +26 -0
  49. data/lib/octocore-cassandra/models/enterprise/category_trend.rb +19 -0
  50. data/lib/octocore-cassandra/models/enterprise/conversions.rb +69 -0
  51. data/lib/octocore-cassandra/models/enterprise/ctr.rb +54 -0
  52. data/lib/octocore-cassandra/models/enterprise/dimension_choice.rb +21 -0
  53. data/lib/octocore-cassandra/models/enterprise/engagement_time.rb +43 -0
  54. data/lib/octocore-cassandra/models/enterprise/funnel_data.rb +20 -0
  55. data/lib/octocore-cassandra/models/enterprise/funnel_tracker.rb +19 -0
  56. data/lib/octocore-cassandra/models/enterprise/funnels.rb +129 -0
  57. data/lib/octocore-cassandra/models/enterprise/gcm.rb +21 -0
  58. data/lib/octocore-cassandra/models/enterprise/newsfeed_hit.rb +52 -0
  59. data/lib/octocore-cassandra/models/enterprise/notification_hit.rb +42 -0
  60. data/lib/octocore-cassandra/models/enterprise/page.rb +15 -0
  61. data/lib/octocore-cassandra/models/enterprise/page_view.rb +14 -0
  62. data/lib/octocore-cassandra/models/enterprise/pageload_time.rb +43 -0
  63. data/lib/octocore-cassandra/models/enterprise/product.rb +22 -0
  64. data/lib/octocore-cassandra/models/enterprise/product_baseline.rb +20 -0
  65. data/lib/octocore-cassandra/models/enterprise/product_hit.rb +26 -0
  66. data/lib/octocore-cassandra/models/enterprise/product_page_view.rb +13 -0
  67. data/lib/octocore-cassandra/models/enterprise/product_trend.rb +18 -0
  68. data/lib/octocore-cassandra/models/enterprise/push_key.rb +15 -0
  69. data/lib/octocore-cassandra/models/enterprise/rules.rb +45 -0
  70. data/lib/octocore-cassandra/models/enterprise/segment.rb +65 -0
  71. data/lib/octocore-cassandra/models/enterprise/segment_data.rb +22 -0
  72. data/lib/octocore-cassandra/models/enterprise/tag.rb +14 -0
  73. data/lib/octocore-cassandra/models/enterprise/tag_baseline.rb +19 -0
  74. data/lib/octocore-cassandra/models/enterprise/tag_hit.rb +26 -0
  75. data/lib/octocore-cassandra/models/enterprise/tag_trend.rb +19 -0
  76. data/lib/octocore-cassandra/models/enterprise/template.rb +18 -0
  77. data/lib/octocore-cassandra/models/plans.rb +17 -0
  78. data/lib/octocore-cassandra/models/subscribe.rb +13 -0
  79. data/lib/octocore-cassandra/models/user.rb +15 -0
  80. data/lib/octocore-cassandra/models/user/push_token.rb +15 -0
  81. data/lib/octocore-cassandra/models/user/user_browser_details.rb +16 -0
  82. data/lib/octocore-cassandra/models/user/user_location_history.rb +15 -0
  83. data/lib/octocore-cassandra/models/user/user_persona.rb +101 -0
  84. data/lib/octocore-cassandra/models/user/user_phone_details.rb +17 -0
  85. data/lib/octocore-cassandra/models/user/user_profile.rb +20 -0
  86. data/lib/octocore-cassandra/models/user/user_timeline.rb +111 -0
  87. data/lib/octocore-cassandra/record.rb +20 -0
  88. data/lib/octocore-cassandra/schedeuleable.rb +20 -0
  89. data/lib/octocore-cassandra/scheduler.rb +72 -0
  90. data/lib/octocore-cassandra/search.rb +5 -0
  91. data/lib/octocore-cassandra/search/client.rb +33 -0
  92. data/lib/octocore-cassandra/search/indexer.rb +0 -0
  93. data/lib/octocore-cassandra/search/searchable.rb +18 -0
  94. data/lib/octocore-cassandra/search/setup.rb +71 -0
  95. data/lib/octocore-cassandra/segment.rb +287 -0
  96. data/lib/octocore-cassandra/stats.rb +33 -0
  97. data/lib/octocore-cassandra/trendable.rb +88 -0
  98. data/lib/octocore-cassandra/trends.rb +158 -0
  99. data/lib/octocore-cassandra/utils.rb +90 -0
  100. data/lib/octocore-cassandra/version.rb +4 -0
  101. data/spec/lib/stats_spec.rb +20 -0
  102. data/spec/spec_helper.rb +103 -0
  103. metadata +490 -0
@@ -0,0 +1,104 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'digest/sha1'
5
+ require 'securerandom'
6
+ require 'octocore-cassandra/models'
7
+
8
+ module Octo
9
+ module Helpers
10
+ module ClientHelper
11
+
12
+ # Create a new Client
13
+ # @param [String] username The name of the client
14
+ # @param [String] email The email of the client
15
+ # @param [String] password The password of the client
16
+ # @return [String] The status of request
17
+ def add_consumer(username, email, password)
18
+ unless enterprise_name_exists?(username)
19
+
20
+ # create enterprise
21
+ e = Octo::Enterprise.new
22
+ e.name = username
23
+ e.save!
24
+
25
+ enterprise_id = e.id.to_s
26
+
27
+ # create its Authentication stuff
28
+ auth = Octo::Authorization.new
29
+ auth.enterprise_id = enterprise_id
30
+ auth.username = e.name
31
+ auth.email = email
32
+ custom_id = enterprise_id
33
+ auth.password = password
34
+ auth.save!
35
+ 'success'
36
+ else
37
+ 'Not creating client as client name exists'
38
+ end
39
+ end
40
+
41
+ # Validate Client authentication
42
+ # @param [String] username The name of the client
43
+ # @param [String] password The password of the client
44
+ # @return [Boolean] Authenticated or not
45
+ def validate_password( username, password)
46
+ consumer = fetch_consumer(username)
47
+ hash_password = Digest::SHA1.hexdigest(password + consumer.enterprise_id)
48
+ hash_password == consumer.password
49
+ end
50
+
51
+ # check enterprise exist
52
+ # @param [String] enterprise_name The name of the enterprise
53
+ # @return [Boolean] Exist or not
54
+ def enterprise_name_exists?(enterprise_name)
55
+ Octo::Enterprise.all.select { |x| x.name == enterprise_name}.length > 0
56
+ end
57
+
58
+ # fetch client data
59
+ # @param [String] username The name of the client
60
+ # @return [Hash] Client Data
61
+ def fetch_consumer(username)
62
+ Octo::Authorization.where(username: username).first
63
+ end
64
+
65
+ # Validate Client session
66
+ # @param [String] username The name of the client
67
+ # @param [String] token The session token of the client
68
+ # @return [Boolean] Authenticated or not
69
+ def validate_token(username, token)
70
+ Octo::Authorization.all.select do |x|
71
+ x.username == username &&
72
+ x.session_token == token
73
+ end.length > 0
74
+ end
75
+
76
+ # Create new client session
77
+ # @param [String] username The name of the client
78
+ # @return [String] Session Token
79
+ def save_session(username)
80
+ consumer = fetch_consumer(username)
81
+ consumer.session_token = SecureRandom.hex
82
+ consumer.save!
83
+ consumer.session_token.to_s
84
+ end
85
+
86
+ # Destroy client session
87
+ # @param [String] username The name of the client
88
+ def destroy_session(username)
89
+ consumer = fetch_consumer(username)
90
+ consumer.session_token = nil
91
+ consumer.save!
92
+ end
93
+
94
+ # check user is admin or client
95
+ # @param [String] username The username of the client
96
+ # @return [Boolean] Is Admin or not
97
+ def check_admin(username)
98
+ consumer = fetch_consumer(username)
99
+ consumer.admin
100
+ end
101
+
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,164 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'digest/sha1'
5
+ require 'securerandom'
6
+
7
+ module Octo
8
+ module Helpers
9
+ module KongHelper
10
+
11
+ # The default URL for kong to connect to
12
+ KONG_URL = 'http://127.0.0.1:8001'
13
+
14
+ # Fetch Kong URL
15
+ # @return [String] Kong URL
16
+ def kong_url
17
+ kong_config = Octo.get_config :kong
18
+ if kong_config.class == String
19
+ kong_config
20
+ elsif kong_config.class == Hash
21
+ kong_config.fetch :url, KONG_URL
22
+ end
23
+ end
24
+
25
+ # Process Every Kong Request
26
+ # @param [String] url The url of the kong request
27
+ # @param [Key] method The request method
28
+ # @param [Hash] payload The request body
29
+ # @return [Hash] Response
30
+ def process_kong_request (url, method, payload)
31
+ begin
32
+ url = kong_url + url
33
+ header = {
34
+ 'Accept' => 'application/json, text/plain, */*',
35
+ 'Content-Type' => 'application/json'
36
+ }
37
+ uri = URI.parse(URI.encode(url.strip))
38
+ http = Net::HTTP.new(uri.host,uri.port)
39
+
40
+ case method
41
+ when :GET
42
+ req = Net::HTTP::Get.new(uri, header) # GET Method
43
+ when :POST
44
+ req = Net::HTTP::Post.new(uri.path, header) # POST Method
45
+ when :PUT
46
+ req = Net::HTTP::Put.new(uri.path, header) # PUT Method
47
+ when :PATCH
48
+ req = Net::HTTP::Patch.new(uri.path, header) # PATCH Method
49
+ when :DELETE
50
+ req = Net::HTTP::Delete.new(uri.path, header) # DELETE Method
51
+ else
52
+ # Default Case
53
+ end
54
+
55
+ puts "HTTP #{ method } to #{ uri.host}:#{ uri.port }. Body: #{ payload.to_json }"
56
+ body = "#{ payload.to_json }"
57
+ res = http.request(req, body)
58
+ if res.body
59
+ JSON.parse(res.body) # Returned Data
60
+ else
61
+ {}
62
+ end
63
+ rescue Exception => e
64
+ puts "Exception: " + e.message.to_s + "\n" + e.backtrace_locations.join("\n") + "}"
65
+ { message: e.to_s }.to_json
66
+ end
67
+ end
68
+
69
+ # Add Key of client for Key Authorization
70
+ # @param [String] username The username of the client
71
+ # @param [String] keyauth_key The Authorization key of the client
72
+ # @return [Hash] Response
73
+ def create_keyauth(username, keyauth_key)
74
+
75
+ url = '/consumers/'+ username +'/key-auth'
76
+ payload = {
77
+ key: keyauth_key
78
+ }
79
+ process_kong_request(url, :POST, payload)
80
+ end
81
+
82
+ # Add a Kong ratelimiting plugin
83
+ # @param [String] apikey The apikey of the client
84
+ # @param [String] consumer_id The consumer_id of the client
85
+ # @param [String] config The configuration of the plugin
86
+ # @return [String] Plugin Id
87
+ def add_ratelimiting_plugin(apikey, consumer_id, config)
88
+
89
+ url = '/apis/' + apikey + '/plugins/'
90
+ payload = {
91
+ name: "rate-limiting",
92
+ consumer_id: consumer_id,
93
+ config: config
94
+ }
95
+
96
+ response = process_kong_request(url, :POST, payload)
97
+
98
+ if response['id']
99
+ response['id'].to_s
100
+ end
101
+ end
102
+
103
+ # List of Client
104
+ # @param [Hash] payload The payload to send
105
+ # @return [Hash] All the clients data
106
+ def consumerlist(payload = {})
107
+ url = '/consumers/'
108
+ res = process_kong_request(url, :GET, payload)
109
+ res['data']
110
+ end
111
+
112
+ # Delete Consumers from Kong
113
+ # @param [String] username The username of the Client
114
+ # @return [String] Status
115
+ def delete_consumer(username)
116
+ url = '/consumers/' + username
117
+ payload = {}
118
+ process_kong_request(url, :DELETE, payload)
119
+ end
120
+
121
+ # List of APIS
122
+ # @param [Hash] payload filter values
123
+ # @return [Hash] All the apis data
124
+ def apislist(payload = {})
125
+ url = '/apis/'
126
+ res = process_kong_request(url, :GET, payload)
127
+ res['data']
128
+ end
129
+
130
+ # Delete apis from Kong
131
+ # @param [String] name The name of the api
132
+ # @return [String] Status
133
+ def delete_api(name)
134
+ url = '/apis/' + name
135
+ payload = {}
136
+ process_kong_request(url, :DELETE, payload)
137
+ end
138
+ end
139
+
140
+ # This class acts as the bridge between Octo and Kong
141
+ class KongBridge
142
+ extend KongHelper
143
+
144
+ class << self
145
+
146
+ # Method to delete all consumers and apis
147
+ def delete_all
148
+ unless apislist.nil?
149
+ apislist.each do |api|
150
+ delete_api(api['name'])
151
+ end
152
+ end
153
+
154
+ unless consumerlist.nil?
155
+ consumerlist.each do |consumer|
156
+ delete_consumer(consumer['username'])
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,22 @@
1
+ require 'octocore-cassandra'
2
+
3
+ module Octo
4
+ module Sinatra
5
+ module Helper
6
+
7
+ # The headers on which kong sends the authenticated details
8
+ KONG_AUTH_HEADERS = %w(HTTP_X_CONSUMER_ID HTTP_X_CONSUMER_CUSTOM_ID HTTP_X_CONSUMER_USERNAME)
9
+
10
+ # Finds the enterprise details
11
+ # #return [Array<String>] Enterprise's Id, Custom Name and User Name
12
+ def enterprise_details
13
+ KONG_AUTH_HEADERS.collect do |prop|
14
+ request.env.fetch(prop, nil)
15
+ end
16
+ end
17
+
18
+
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,60 @@
1
+ require 'ruby-kafka'
2
+
3
+ module Octo
4
+ # The bridge between Kafka and ruby
5
+ class KafkaBridge
6
+
7
+ # These are hard wired
8
+ CLIENT_ID = ENV['KAFKA_CLIENT_ID']
9
+ TOPIC = ENV['KAFKA_TOPIC']
10
+
11
+ MAX_BUFFER_SIZE = 20_000
12
+
13
+ MAX_QUEUE_SIZE = 10_000
14
+
15
+ DELIVERY_INTERVAL = 1
16
+
17
+ # Changes as per environment
18
+ BROKERS = ENV['KAFKA_BROKERS'].try(:split, ',')
19
+
20
+ def initialize(opts = {})
21
+ opts.deep_symbolize_keys!
22
+ @kafka = ::Kafka.new(seed_brokers: opts.fetch(:brokers, BROKERS),
23
+ client_id: opts.fetch(:client_id, CLIENT_ID)
24
+ )
25
+ @producer = @kafka.async_producer(
26
+ max_buffer_size: opts.fetch(:max_buffer_size, MAX_BUFFER_SIZE),
27
+ max_queue_size: opts.fetch(:max_queue_size, MAX_QUEUE_SIZE),
28
+ delivery_interval: opts.fetch(:delivery_interval, DELIVERY_INTERVAL),
29
+ )
30
+ if opts.has_key?(:topic)
31
+ @topic = opts[:topic]
32
+ else
33
+ @topic = TOPIC
34
+ end
35
+ end
36
+
37
+ def push(params)
38
+ create_message params
39
+ end
40
+
41
+ def teardown
42
+ @producer.shutdown
43
+ end
44
+
45
+ private
46
+
47
+ # Creates a new message.
48
+ # @param [Hash] message The message hash to be produced
49
+ def create_message(message)
50
+ begin
51
+ @producer.produce(JSON.dump(message), topic: @topic)
52
+ rescue Kafka::BufferOverflow
53
+ Octo.logger.error 'Buffer Overflow. Sleeping for 1s'
54
+ sleep 1
55
+ retry
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ module Octo
2
+
3
+ module KLDivergence
4
+
5
+ # Calculates the KL-Divergance of two probabilities
6
+ # https://en.wikipedia.org/wiki/Kullback–Leibler_divergence
7
+ # @param [Float] p The first or observed probability
8
+ # @param [Float] q The second or believed probability. Must be non-zero
9
+ # @return [Float] KL-Divergance score
10
+ def kl_divergence(p, q)
11
+ p * Math.log(p/q)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1 @@
1
+ require 'octocore-cassandra/mailer/subscriber_mailer'
@@ -0,0 +1,30 @@
1
+ require 'octocore-cassandra'
2
+
3
+ module Octo
4
+ module Mailer
5
+ class SubscriberMailer
6
+ @queue = :subscriber_notifier
7
+
8
+ # Method for the scheduler to call
9
+ # Counts the number of subscriber in the last 24 hours
10
+ # and then sends a mail with subscriber count to the
11
+ # email mentioned
12
+
13
+ def perform (from=nil)
14
+ if from.nil?
15
+ subscribers = Octo::Subscriber.where(created_at: 24.hours.ago..Time.now.floor)
16
+ else
17
+ subscribers = Octo::Subscriber.where(created_at: from..Time.now.floor)
18
+ end
19
+ # MAIL CODE
20
+ Octo.get_config(:email_to).each { |x|
21
+ opts1 = {
22
+ text: "Today number of new susbcribes are " + subscribers.length,
23
+ name: x.fetch('name')
24
+ }
25
+ Octo::Email.send(x.fetch('email'), subject, opts1)
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,114 @@
1
+ require 'json'
2
+
3
+ module Octo
4
+ # Message abstraction module
5
+ module Message
6
+
7
+ # Parsing kafka messages for octo consumer
8
+ module MessageParser
9
+
10
+ # Parsing Message hash in Octo compatible form
11
+ # @param [Hash] Message Hash
12
+ # @return [Hash] Hash in Octo form
13
+ def parse(msg)
14
+ msg = JSON.parse(msg)
15
+ m = { event_name: msg['event_name'] }
16
+ case msg['event_name']
17
+ when 'funnel_update'
18
+ m.merge!({
19
+ rediskey: msg['rediskey']
20
+ })
21
+ when 'update.profile'
22
+ m.merge!({
23
+ profileDetails: msg['profileDetails']
24
+ })
25
+ when 'page.view'
26
+ m.merge!({
27
+ routeUrl: msg['routeUrl'],
28
+ categories: msg.fetch('categories', []),
29
+ tags: msg.fetch('tags', [])
30
+ })
31
+ when 'productpage.view'
32
+ m.merge!({
33
+ routeUrl: msg['routeUrl'],
34
+ categories: msg.fetch('categories', []),
35
+ tags: msg.fetch('tags', []),
36
+ productId: msg['productId'],
37
+ productName: msg['productName'],
38
+ price: msg['price']
39
+ })
40
+ when 'update.push_token'
41
+ m.merge!({
42
+ pushType: msg['notificationType'],
43
+ pushKey: msg['pushKey'],
44
+ pushToken: msg['pushToken']
45
+ })
46
+ end
47
+ enterprise = msg['enterprise']
48
+ raise StandardError, 'Parse Error' if enterprise.nil?
49
+
50
+ eid = if enterprise.has_key?'custom_id'
51
+ enterprise['custom_id']
52
+ elsif enterprise.has_key?'customId'
53
+ enterprise['customId']
54
+ end
55
+
56
+ ename = if enterprise.has_key?'user_name'
57
+ enterprise['user_name']
58
+ elsif enterprise.has_key?'userName'
59
+ enterprise['userName']
60
+ else
61
+ nil
62
+ end
63
+ m.merge!({
64
+ id: msg.fetch('uuid', nil),
65
+ enterpriseId: eid,
66
+ enterpriseName: ename,
67
+ phone: msg.fetch('phoneDetails', nil),
68
+ browser: msg.fetch('browserDetails', nil),
69
+ userId: msg.fetch('userId', -1),
70
+ created_at: Time.now
71
+ })
72
+
73
+ m
74
+ end
75
+
76
+ end
77
+
78
+ # To handle message abstraction
79
+ class Message
80
+ include MessageParser
81
+
82
+ attr_reader :message
83
+
84
+ # Converting Message hash in Octo compatible form
85
+ # @param [Hash] Message Hash
86
+ def initialize(msg)
87
+ @message = msg
88
+ end
89
+
90
+ # To get hash message
91
+ # @return [Hash] Message Hash
92
+ def to_h
93
+ parse(@message)
94
+ end
95
+
96
+ # To get enterprise id
97
+ # @return [String] Enterprise Id
98
+ def eid
99
+ msg = to_json
100
+ enterprise = msg['enterprise']
101
+ raise StandardError, 'Parse Error' if enterprise.nil?
102
+
103
+ eid = if enterprise.has_key?'custom_id'
104
+ enterprise['custom_id']
105
+ elsif enterprise.has_key?'customId'
106
+ enterprise['customId']
107
+ end
108
+
109
+ eid
110
+ end
111
+
112
+ end
113
+ end
114
+ end