couchbase-jruby-client 0.1.1

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.jrubyrc +722 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +203 -0
  7. data/README.md +349 -0
  8. data/Rakefile +10 -0
  9. data/couchbase-jruby-client.gemspec +31 -0
  10. data/lib/couchbase/async/callback.rb +19 -0
  11. data/lib/couchbase/async/queue.rb +26 -0
  12. data/lib/couchbase/async.rb +140 -0
  13. data/lib/couchbase/bucket.rb +556 -0
  14. data/lib/couchbase/cluster.rb +105 -0
  15. data/lib/couchbase/constants.rb +12 -0
  16. data/lib/couchbase/design_doc.rb +61 -0
  17. data/lib/couchbase/error.rb +43 -0
  18. data/lib/couchbase/jruby/couchbase_client.rb +22 -0
  19. data/lib/couchbase/jruby/future.rb +8 -0
  20. data/lib/couchbase/operations/arithmetic.rb +301 -0
  21. data/lib/couchbase/operations/delete.rb +104 -0
  22. data/lib/couchbase/operations/design_docs.rb +99 -0
  23. data/lib/couchbase/operations/get.rb +282 -0
  24. data/lib/couchbase/operations/stats.rb +26 -0
  25. data/lib/couchbase/operations/store.rb +461 -0
  26. data/lib/couchbase/operations/touch.rb +136 -0
  27. data/lib/couchbase/operations/unlock.rb +192 -0
  28. data/lib/couchbase/operations/utils.rb +44 -0
  29. data/lib/couchbase/operations.rb +27 -0
  30. data/lib/couchbase/query.rb +73 -0
  31. data/lib/couchbase/result.rb +43 -0
  32. data/lib/couchbase/transcoder.rb +77 -0
  33. data/lib/couchbase/utils.rb +62 -0
  34. data/lib/couchbase/version.rb +3 -0
  35. data/lib/couchbase/view.rb +367 -0
  36. data/lib/couchbase/view_row.rb +193 -0
  37. data/lib/couchbase.rb +157 -0
  38. data/lib/jars/commons-codec-1.5.jar +0 -0
  39. data/lib/jars/couchbase-client-1.2.0-javadoc.jar +0 -0
  40. data/lib/jars/couchbase-client-1.2.0-sources.jar +0 -0
  41. data/lib/jars/couchbase-client-1.2.0.jar +0 -0
  42. data/lib/jars/httpcore-4.1.1.jar +0 -0
  43. data/lib/jars/httpcore-nio-4.1.1.jar +0 -0
  44. data/lib/jars/jettison-1.1.jar +0 -0
  45. data/lib/jars/netty-3.5.5.Final.jar +0 -0
  46. data/lib/jars/spymemcached-2.10.0-javadoc.jar +0 -0
  47. data/lib/jars/spymemcached-2.10.0-sources.jar +0 -0
  48. data/lib/jars/spymemcached-2.10.0.jar +0 -0
  49. data/test/profile/.gitignore +1 -0
  50. data/test/profile/.jrubyrc +722 -0
  51. data/test/profile/Gemfile +6 -0
  52. data/test/profile/benchmark.rb +168 -0
  53. data/test/profile/profile.rb +59 -0
  54. data/test/setup.rb +203 -0
  55. data/test/test_arithmetic.rb +177 -0
  56. data/test/test_async.rb +324 -0
  57. data/test/test_bucket.rb +213 -0
  58. data/test/test_cas.rb +79 -0
  59. data/test/test_couchbase.rb +29 -0
  60. data/test/test_couchbase_rails_cache_store.rb +341 -0
  61. data/test/test_delete.rb +125 -0
  62. data/test/test_design_docs.rb +72 -0
  63. data/test/test_errors.rb +82 -0
  64. data/test/test_format.rb +161 -0
  65. data/test/test_get.rb +417 -0
  66. data/test/test_query.rb +23 -0
  67. data/test/test_stats.rb +57 -0
  68. data/test/test_store.rb +213 -0
  69. data/test/test_timer.rb +43 -0
  70. data/test/test_touch.rb +97 -0
  71. data/test/test_unlock.rb +121 -0
  72. data/test/test_utils.rb +58 -0
  73. data/test/test_version.rb +53 -0
  74. data/test/test_view.rb +94 -0
  75. metadata +255 -0
@@ -0,0 +1,367 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'base64'
19
+
20
+ module Couchbase
21
+
22
+ module Error
23
+ class View < Base
24
+ attr_reader :from, :reason
25
+
26
+ def initialize(from, reason, prefix = "SERVER: ")
27
+ @from = from
28
+ @reason = reason
29
+ super("#{prefix}#{from}: #{reason}")
30
+ end
31
+ end
32
+
33
+ class HTTP < Base
34
+ attr_reader :type, :reason
35
+
36
+ def parse_body!
37
+ if @body
38
+ hash = MultiJson.load(@body)
39
+ if hash["errors"]
40
+ @type = :invalid_arguments
41
+ @reason = hash["errors"].values.join(" ")
42
+ else
43
+ @type = hash["error"]
44
+ @reason = hash["reason"]
45
+ end
46
+ end
47
+ rescue MultiJson::DecodeError
48
+ @type = @reason = nil
49
+ end
50
+
51
+ def to_s
52
+ str = super
53
+ if @type || @reason
54
+ str.sub(/ \(/, ": #{[@type, @reason].compact.join(": ")} (")
55
+ else
56
+ str
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # This class implements Couchbase View execution
63
+ #
64
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views.html
65
+ class View
66
+
67
+ include Enumerable
68
+ include Constants
69
+
70
+ class ArrayWithTotalRows < Array # :nodoc:
71
+ attr_accessor :total_rows
72
+ alias total_entries total_rows
73
+ end
74
+
75
+ attr_reader :params, :design_doc, :name
76
+
77
+ # Set up view endpoint and optional params
78
+ #
79
+ # @param [Couchbase::Bucket] bucket Connection object which
80
+ # stores all info about how to make requests to Couchbase views.
81
+ #
82
+ # @param [String] endpoint Full Couchbase View URI.
83
+ #
84
+ # @param [Hash] params Optional parameter which will be passed to
85
+ # {View#fetch}
86
+ #
87
+ def initialize(bucket, endpoint, params = {})
88
+ @bucket = bucket
89
+ @endpoint = endpoint
90
+ @design_doc, @name = parse_endpoint(endpoint)
91
+ @params = { :connection_timeout => 75_000 }.merge(params)
92
+ @wrapper_class = params.delete(:wrapper_class) || ViewRow
93
+ unless @wrapper_class.respond_to?(:wrap)
94
+ raise ArgumentError, "wrapper class should reposond to :wrap, check the options"
95
+ end
96
+ end
97
+
98
+ # Yields each document that was fetched by view. It doesn't instantiate
99
+ # all the results because of streaming JSON parser. Returns Enumerator
100
+ # unless block given.
101
+ #
102
+ # @param [Hash] params Params for Couchdb query. Some useful are:
103
+ # :start_key, :start_key_doc_id, :descending. See {View#fetch}.
104
+ #
105
+ # @example Use each method with block
106
+ #
107
+ # view.each do |doc|
108
+ # # do something with doc
109
+ # end
110
+ #
111
+ # @example Use Enumerator version
112
+ #
113
+ # enum = view.each # request hasn't issued yet
114
+ # enum.map{|doc| doc.title.upcase}
115
+ #
116
+ # @example Pass options during view initialization
117
+ #
118
+ # endpoint = "http://localhost:5984/default/_design/blog/_view/recent"
119
+ # view = View.new(conn, endpoint, :descending => true)
120
+ # view.each do |document|
121
+ # # do something with document
122
+ # end
123
+ #
124
+ def each(params = {})
125
+ return enum_for(:each, params) unless block_given?
126
+ fetch(params) { |doc| yield(doc) }
127
+ end
128
+
129
+ def first(params = {})
130
+ params = params.merge(:limit => 1)
131
+ fetch(params).first
132
+ end
133
+
134
+ def take(n, params = {})
135
+ params = params.merge(:limit => n)
136
+ fetch(params)
137
+ end
138
+
139
+ # Registers callback function for handling error objects in view
140
+ # results stream.
141
+ #
142
+ # @yieldparam [String] from Location of the node where error occured
143
+ # @yieldparam [String] reason The reason message describing what
144
+ # happened.
145
+ #
146
+ # @example Using +#on_error+ to log all errors in view result
147
+ #
148
+ # # JSON-encoded view result
149
+ # #
150
+ # # {
151
+ # # "total_rows": 0,
152
+ # # "rows": [ ],
153
+ # # "errors": [
154
+ # # {
155
+ # # "from": "127.0.0.1:5984",
156
+ # # "reason": "Design document `_design/testfoobar` missing in database `test_db_b`."
157
+ # # },
158
+ # # {
159
+ # # "from": "http:// localhost:5984/_view_merge/",
160
+ # # "reason": "Design document `_design/testfoobar` missing in database `test_db_c`."
161
+ # # }
162
+ # # ]
163
+ # # }
164
+ #
165
+ # view.on_error do |from, reason|
166
+ # logger.warn("#{view.inspect} received the error '#{reason}' from #{from}")
167
+ # end
168
+ # docs = view.fetch
169
+ #
170
+ # @example More concise example to just count errors
171
+ #
172
+ # errcount = 0
173
+ # view.on_error{|f,r| errcount += 1}.fetch
174
+ #
175
+ def on_error(&callback)
176
+ @on_error = callback
177
+ self # enable call chains
178
+ end
179
+
180
+ # Performs query to Couchbase view. This method will stream results if block
181
+ # given or return complete result set otherwise. In latter case it defines
182
+ # method +total_rows+ returning corresponding entry from
183
+ # Couchbase result object.
184
+ #
185
+ # @note Avoid using +$+ symbol as prefix for properties in your
186
+ # documents, because server marks with it meta fields like flags and
187
+ # expiration, therefore dollar prefix is some kind of reserved. It
188
+ # won't hurt your application. Currently the {ViewRow}
189
+ # class extracts +$flags+, +$cas+ and +$expiration+ properties from
190
+ # the document and store them in {ViewRow#meta} hash.
191
+ #
192
+ # @param [Hash] params parameters for Couchbase query.
193
+ # @option params [true, false] :include_docs (false) Include the
194
+ # full content of the documents in the return. Note that the document
195
+ # is fetched from the in memory cache where it may have been changed
196
+ # or even deleted. See also +:quiet+ parameter below to control error
197
+ # reporting during fetch.
198
+ # @option params [true, false] :quiet (true) Do not raise error if
199
+ # associated document not found in the memory. If the parameter +true+
200
+ # will use +nil+ value instead.
201
+ # @option params [true, false] :descending (false) Return the documents
202
+ # in descending by key order
203
+ # @option params [String, Fixnum, Hash, Array] :key Return only
204
+ # documents that match the specified key. Will be JSON encoded.
205
+ # @option params [Array] :keys The same as +:key+, but will work for
206
+ # set of keys. Will be JSON encoded.
207
+ # @option params [String, Fixnum, Hash, Array] :startkey Return
208
+ # records starting with the specified key. +:start_key+ option should
209
+ # also work here. Will be JSON encoded.
210
+ # @option params [String] :startkey_docid Document id to start with
211
+ # (to allow pagination for duplicate startkeys). +:start_key_doc_id+
212
+ # also should work.
213
+ # @option params [String, Fixnum, Hash, Array] :endkey Stop returning
214
+ # records when the specified key is reached. +:end_key+ option should
215
+ # also work here. Will be JSON encoded.
216
+ # @option params [String] :endkey_docid Last document id to include
217
+ # in the output (to allow pagination for duplicate startkeys).
218
+ # +:end_key_doc_id+ also should work.
219
+ # @option params [true, false] :inclusive_end (true) Specifies whether
220
+ # the specified end key should be included in the result
221
+ # @option params [Fixnum] :limit Limit the number of documents in the
222
+ # output.
223
+ # @option params [Fixnum] :skip Skip this number of records before
224
+ # starting to return the results.
225
+ # @option params [String, Symbol] :on_error (:continue) Sets the
226
+ # response in the event of an error. Supported values:
227
+ # :continue:: Continue to generate view information in the event of an
228
+ # error, including the error information in the view
229
+ # response stream.
230
+ # :stop:: Stop immediately when an error condition occurs. No
231
+ # further view information will be returned.
232
+ # @option params [Fixnum] :connection_timeout (75000) Timeout before the
233
+ # view request is dropped (milliseconds)
234
+ # @option params [true, false] :reduce (true) Use the reduction function
235
+ # @option params [true, false] :group (false) Group the results using
236
+ # the reduce function to a group or single row.
237
+ # @option params [Fixnum] :group_level Specify the group level to be
238
+ # used.
239
+ # @option params [String, Symbol, false] :stale (:update_after) Allow
240
+ # the results from a stale view to be used. Supported values:
241
+ # false:: Force a view update before returning data
242
+ # :ok:: Allow stale views
243
+ # :update_after:: Allow stale view, update view after it has been
244
+ # accessed
245
+ # @option params [Hash] :body Accepts the same parameters, except
246
+ # +:body+ of course, but sends them in POST body instead of query
247
+ # string. It could be useful for really large and complex parameters.
248
+ #
249
+ # @yieldparam [Couchbase::ViewRow] document
250
+ #
251
+ # @return [Array] with documents. There will be +total_entries+
252
+ # method defined on this array if it's possible.
253
+ #
254
+ # @raise [Couchbase::Error::View] when +on_error+ callback is nil and
255
+ # error object found in the result stream.
256
+ #
257
+ # @example Query +recent_posts+ view with key filter
258
+ # doc.recent_posts(:body => {:keys => ["key1", "key2"]})
259
+ #
260
+ # @example Fetch second page of result set (splitted in 10 items per page)
261
+ # page = 2
262
+ # per_page = 10
263
+ # doc.recent_posts(:skip => (page - 1) * per_page, :limit => per_page)
264
+ #
265
+ # @example Simple join using Map/Reduce
266
+ # # Given the bucket with Posts(:id, :type, :title, :body) and
267
+ # # Comments(:id, :type, :post_id, :author, :body). The map function
268
+ # # below (in javascript) will build the View index called
269
+ # # "recent_posts_with_comments" which will behave like left inner join.
270
+ # #
271
+ # # function(doc) {
272
+ # # switch (doc.type) {
273
+ # # case "Post":
274
+ # # emit([doc.id, 0], null);
275
+ # # break;
276
+ # # case "Comment":
277
+ # # emit([doc.post_id, 1], null);
278
+ # # break;
279
+ # # }
280
+ # # }
281
+ # #
282
+ # post_id = 42
283
+ # doc.recent_posts_with_comments(:start_key => [post_id, 0],
284
+ # :end_key => [post_id, 1],
285
+ # :include_docs => true)
286
+ def fetch(params = {})
287
+ params = @params.merge(params)
288
+ include_docs = params[:include_docs]
289
+ quiet = params.fetch(:quiet, true)
290
+
291
+ view = @bucket.client.getView(@design_doc, @name)
292
+
293
+ query = Query.new(params)
294
+
295
+ request = @bucket.client.query(view, query.generate)
296
+
297
+ if block_given?
298
+ block = Proc.new
299
+ request.each do |data|
300
+ doc = @wrapper_class.wrap(@bucket, data)
301
+ block.call(doc)
302
+ end
303
+ nil
304
+ else
305
+ docs = request.to_a.map { |data|
306
+ @wrapper_class.wrap(@bucket, data)
307
+ }
308
+ docs = ArrayWithTotalRows.new(docs)
309
+ docs.total_rows = request.size
310
+ docs
311
+ end
312
+ end
313
+
314
+ # Method for fetching asynchronously all rows and passing array to callback
315
+ #
316
+ # Parameters are same as for {View#fetch} method, but callback is called for whole set for
317
+ # rows instead of one by each.
318
+ #
319
+ # @example
320
+ # con.run do
321
+ # doc.recent_posts.fetch_all do |posts|
322
+ # do_something_with_all_posts(posts)
323
+ # end
324
+ # end
325
+ def fetch_all(params = {}, &block)
326
+ return fetch(params) unless @bucket.async?
327
+ raise ArgumentError, "Block needed for fetch_all in async mode" unless block
328
+
329
+ all = []
330
+ fetch(params) do |row|
331
+ all << row
332
+ if row.last?
333
+ @bucket.create_timer(0) { block.call(all) }
334
+ end
335
+ end
336
+ end
337
+
338
+
339
+ # Returns a string containing a human-readable representation of the {View}
340
+ #
341
+ # @return [String]
342
+ def inspect
343
+ %(#<#{self.class.name}:#{self.object_id} @endpoint=#{@endpoint.inspect} @params=#{@params.inspect}>)
344
+ end
345
+
346
+ private
347
+
348
+ def send_error(*args)
349
+ if @on_error
350
+ @on_error.call(*args.take(2))
351
+ else
352
+ raise Error::View.new(*args)
353
+ end
354
+ end
355
+
356
+ private
357
+
358
+ def parse_endpoint(endpoint)
359
+ parts = endpoint.split('/')
360
+ if endpoint =~ /^_design/
361
+ [parts[1], parts[3]]
362
+ else
363
+ [parts[0], parts[2]]
364
+ end
365
+ end
366
+ end
367
+ end
@@ -0,0 +1,193 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011-2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+ # This class encapsulates structured JSON document
20
+ #
21
+ # @since 1.2.0
22
+ #
23
+ # It behaves like Hash for document included into row, and has access methods to row data as well.
24
+ #
25
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-datastore.html
26
+ class ViewRow
27
+
28
+ java_import com.couchbase.client.protocol.views.ViewRowNoDocs
29
+ java_import com.couchbase.client.protocol.views.ViewRowWithDocs
30
+ java_import com.couchbase.client.protocol.views.ViewRowReduced
31
+ java_import com.couchbase.client.protocol.views.SpatialViewRowNoDocs
32
+ java_import com.couchbase.client.protocol.views.SpatialViewRowWithDocs
33
+
34
+ include Constants
35
+
36
+ # Undefine as much methods as we can to free names for views
37
+ instance_methods.each do |m|
38
+ undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$|^class$|)/
39
+ end
40
+
41
+ # The hash built from JSON document.
42
+ #
43
+ # @since 1.2.0
44
+ #
45
+ # This is complete response from the Couchbase
46
+ #
47
+ # @return [Hash]
48
+ attr_accessor :data
49
+
50
+ # The key which was emitted by map function
51
+ #
52
+ # @since 1.2.0
53
+ #
54
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-map.html
55
+ #
56
+ # Usually it is String (the object +_id+) but it could be also any
57
+ # compount JSON value.
58
+ #
59
+ # @return [Object]
60
+ attr_accessor :key
61
+
62
+ # The value which was emitted by map function
63
+ #
64
+ # @since 1.2.0
65
+ #
66
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-map.html
67
+ #
68
+ # @return [Object]
69
+ attr_accessor :value
70
+
71
+ # The document hash.
72
+ #
73
+ # @since 1.2.0
74
+ #
75
+ # It usually available when view executed with +:include_doc+ argument.
76
+ #
77
+ # @return [Hash]
78
+ attr_accessor :doc
79
+
80
+ # The identificator of the document
81
+ #
82
+ # @since 1.2.0
83
+ #
84
+ # @return [String]
85
+ attr_accessor :id
86
+
87
+ # The meta data linked to the document
88
+ #
89
+ # @since 1.2.0
90
+ #
91
+ # @return [Hash]
92
+ attr_accessor :meta
93
+
94
+ # Initialize the document instance
95
+ #
96
+ # @since 1.2.0
97
+ #
98
+ # It takes reference to the bucket, data hash.
99
+ #
100
+ # @param [Couchbase::Bucket] bucket the reference to connection
101
+ # @param [Hash] data the data hash, which was built from JSON document
102
+ # representation
103
+ def initialize(bucket, data)
104
+ @bucket = bucket
105
+ @data = data
106
+ @key = data.key
107
+ @value = data.value
108
+ @id = data.id
109
+ @last = false
110
+
111
+ case data
112
+ when ViewRowWithDocs, SpatialViewRowWithDocs
113
+ @doc = data.document
114
+ when SpatialViewRowNoDocs, SpatialViewRowWithDocs
115
+ @geometry = data.geometry
116
+ @bbox = data.bbox
117
+ end
118
+ end
119
+
120
+ # Wraps data hash into ViewRow instance
121
+ #
122
+ # @since 1.2.0
123
+ #
124
+ # @see ViewRow#initialize
125
+ #
126
+ # @param [Couchbase::Bucket] bucket the reference to connection
127
+ # @param [Hash] data the data hash, which was built from JSON document
128
+ # representation
129
+ #
130
+ # @return [ViewRow]
131
+ def self.wrap(bucket, data)
132
+ self.new(bucket, data)
133
+ end
134
+
135
+ # Get attribute of the document
136
+ #
137
+ # @since 1.2.0
138
+ #
139
+ # Fetches attribute from underlying document hash
140
+ #
141
+ # @param [String] key the attribute name
142
+ #
143
+ # @return [Object] property value or nil
144
+ def [](key)
145
+ @doc[key]
146
+ end
147
+
148
+ # Check attribute existence
149
+ #
150
+ # @since 1.2.0
151
+ #
152
+ # @param [String] key the attribute name
153
+ #
154
+ # @return [true, false] +true+ if the given attribute is present in in
155
+ # the document.
156
+ def has_key?(key)
157
+ @doc.has_key?(key)
158
+ end
159
+
160
+ # Set document attribute
161
+ #
162
+ # @since 1.2.0
163
+ #
164
+ # Set or update the attribute in the document hash
165
+ #
166
+ # @param [String] key the attribute name
167
+ # @param [Object] value the attribute value
168
+ #
169
+ # @return [Object] the value
170
+ def []=(key, value)
171
+ @doc[key] = value
172
+ end
173
+
174
+ # Signals if this row is last in a stream
175
+ #
176
+ # @since 1.2.1
177
+ #
178
+ # @return [true, false] +true+ if this row is last in a stream
179
+ def last?
180
+ @last
181
+ end
182
+
183
+ def inspect
184
+ desc = "#<#{self.class.name}:#{self.object_id}"
185
+ [:@id, :@key, :@value, :@doc, :@meta].each do |iv|
186
+ desc << " #{iv}=#{instance_variable_get(iv).inspect}"
187
+ end
188
+ desc << ">"
189
+ desc
190
+ end
191
+ end
192
+
193
+ end