appengine-apis 0.0.2 → 0.0.3

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,3 +1,9 @@
1
+ == 0.0.3 2009-04-25
2
+
3
+ * Added Memcache and Mail APIs
4
+ * Tries to automatically add SDK to your classpath if it's missing.
5
+ * Added AppEngine::Testing.boot to configure APIs for using in IRB.
6
+
1
7
  == 0.0.2 2009-04-15
2
8
 
3
9
  * Add URLFetch and Users APIs
@@ -7,8 +7,12 @@ lib/appengine-apis.rb
7
7
  lib/appengine-apis/apiproxy.rb
8
8
  lib/appengine-apis/datastore.rb
9
9
  lib/appengine-apis/datastore_types.rb
10
+ lib/appengine-apis/local_boot.rb
10
11
  lib/appengine-apis/logger.rb
12
+ lib/appengine-apis/mail.rb
13
+ lib/appengine-apis/memcache.rb
11
14
  lib/appengine-apis/merb-logger.rb
15
+ lib/appengine-apis/sdk.rb
12
16
  lib/appengine-apis/testing.rb
13
17
  lib/appengine-apis/urlfetch.rb
14
18
  lib/appengine-apis/users.rb
@@ -18,6 +22,8 @@ script/generate
18
22
  spec/datastore_spec.rb
19
23
  spec/datastore_types_spec.rb
20
24
  spec/logger_spec.rb
25
+ spec/mail_spec.rb
26
+ spec/memcache_spec.rb
21
27
  spec/spec.opts
22
28
  spec/spec_helper.rb
23
29
  spec/urlfetch_spec.rb
@@ -6,10 +6,20 @@
6
6
 
7
7
  APIs and utilities for using JRuby on Google App Engine.
8
8
 
9
+ To load the API stubs in IRB simply
10
+ require 'rubygems'
11
+ require 'appengine-apis/local_boot'
12
+
13
+ This will configure access to the same Datastore as running
14
+
15
+ $ dev_appserver.sh .
16
+
9
17
  See these classes for an overview of each API:
10
18
  - AppEngine::Logger
11
19
  - AppEngine::Testing
12
20
  - AppEngine::Users
21
+ - AppEngine::Mail
22
+ - AppEngine::Memcache
13
23
  - AppEngine::URLFetch
14
24
  - AppEngine::Datastore
15
25
 
@@ -28,7 +38,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
28
38
  you may not use this file except in compliance with the License.
29
39
  You may obtain a copy of the License at
30
40
 
31
- http://www.apache.org/licenses/LICENSE-2.0
41
+ http://www.apache.org/licenses/LICENSE-2.0
32
42
 
33
43
  Unless required by applicable law or agreed to in writing, software
34
44
  distributed under the License is distributed on an "AS IS" BASIS,
data/Rakefile CHANGED
@@ -1,6 +1,14 @@
1
1
  %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
2
  require File.dirname(__FILE__) + '/lib/appengine-apis'
3
3
 
4
+ # set up pretty rdoc if possible
5
+ begin
6
+ gem 'rdoc'
7
+ require 'sdoc'
8
+ ENV['RDOCOPT'] = '-T lightblue'
9
+ rescue
10
+ end
11
+
4
12
  # Generate all the Rake tasks
5
13
  # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
14
  $hoe = Hoe.new('appengine-apis', AppEngine::VERSION) do |p|
@@ -19,5 +19,5 @@ $:.unshift(File.dirname(__FILE__)) unless
19
19
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
20
20
 
21
21
  module AppEngine
22
- VERSION = '0.0.2'
22
+ VERSION = '0.0.3'
23
23
  end
@@ -18,11 +18,11 @@
18
18
  #
19
19
  # Ruby interface to the Java ApiProxy.
20
20
 
21
- require 'java'
21
+ require 'appengine-apis/sdk'
22
22
 
23
23
  module AppEngine
24
24
 
25
- import Java.com.google.apphosting.api.ApiProxy
25
+ ApiProxy = AppEngine::SDK.load_apiproxy
26
26
 
27
27
  class << ApiProxy
28
28
  def get_app_id
@@ -24,7 +24,6 @@
24
24
  #
25
25
  # The datastore errors are defined in datastore_types.rb.
26
26
 
27
- require 'appengine-apis/apiproxy'
28
27
  require 'appengine-apis/datastore_types'
29
28
 
30
29
  module AppEngine
@@ -60,7 +59,7 @@ module AppEngine
60
59
  # - Datastore::Text
61
60
  # - Datastore::Blob
62
61
  # - Datastore::ByteString
63
- # - com.google.appengine.api.users.User
62
+ # - Users::User
64
63
 
65
64
  module Datastore
66
65
  module_function
@@ -145,15 +144,15 @@ module Datastore
145
144
  # current transaction and will be returned by subsequent, same-thread
146
145
  # calls to #current_transaction until one of the following happens:
147
146
  #
148
- # 1. #begin_transaction is invoked from the same thread. In this case
149
- # #current_transaction will return the result of the more recent
150
- # call to #begin_transaction.
151
- # 2. #Transaction.commit is invoked on the Transaction returned by
152
- # this method. Whether or not the commit succeeds, the
153
- # Transaction will no longer be current.
154
- # 3. #Transaction.rollback is invoked on the Transaction returned by
155
- # this method. Whether or not the rollback succeeds, the
156
- # Transaction will no longer be current.
147
+ # 1. begin_transaction is invoked from the same thread. In this case
148
+ # current_transaction will return the result of the more recent
149
+ # call to begin_transaction.
150
+ # 2. Transaction.commit is invoked on the Transaction returned by
151
+ # this method. Whether or not the commit succeeds, the
152
+ # Transaction will no longer be current.
153
+ # 3. Transaction.rollback is invoked on the Transaction returned by
154
+ # this method. Whether or not the rollback succeeds, the
155
+ # Transaction will no longer be current.
157
156
  #
158
157
  def begin_transaction
159
158
  convert_exceptions do
@@ -125,7 +125,7 @@ module AppEngine
125
125
  # across all apps, and includes all information necessary to fetch
126
126
  # the entity from the datastore with #Datastore.get(Key).
127
127
  #
128
- # See http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Key.html
128
+ # See also http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Key.html
129
129
  #
130
130
  class Key
131
131
 
@@ -227,7 +227,7 @@ module AppEngine
227
227
  # as an arbitrary string), and a set of zero or more typed
228
228
  # properties.
229
229
  #
230
- # See http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Entity.html
230
+ # See also http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Entity.html
231
231
  #
232
232
  class Entity
233
233
  include Enumerable
@@ -306,7 +306,7 @@ module AppEngine
306
306
  # Add the properties from +other+ to this Entity.
307
307
  # Other may be an Entity or Hash
308
308
  def update(other)
309
- hash.each do |name, value|
309
+ other.each do |name, value|
310
310
  self[name] = value
311
311
  end
312
312
  self
@@ -317,6 +317,14 @@ module AppEngine
317
317
  def each(&proc) # :yields: name, value
318
318
  getProperties.each(&proc)
319
319
  end
320
+
321
+ def to_hash
322
+ inject({}) do |hash, item|
323
+ name, value = item
324
+ hash[name] = value
325
+ hash
326
+ end
327
+ end
320
328
  end
321
329
 
322
330
  SPECIAL_RUBY_TYPES = [Time, Text, Blob, ByteString, Link].freeze
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/ruby1.8 -w
2
+ #
3
+ # Copyright:: Copyright 2009 Google Inc.
4
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ #
19
+ # Helpers for installing stub apis in unit tests.
20
+
21
+ require 'appengine-apis/testing'
22
+ AppEngine::Testing.boot
23
+
24
+ require 'appengine-apis/datastore'
25
+ require 'appengine-apis/logger'
26
+ require 'appengine-apis/mail'
27
+ require 'appengine-apis/memcache'
28
+ require 'appengine-apis/urlfetch'
29
+ require 'appengine-apis/users'
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/ruby1.8 -w
2
+ #
3
+ # Copyright:: Copyright 2009 Google Inc.
4
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module AppEngine
20
+
21
+ # App Engine applications can send email messages on behalf of the app's
22
+ # administrators, and on behalf of users with Google Accounts. Apps use the
23
+ # Mail service to send email messages.
24
+ #
25
+ # The Mail.send method sends an email message from the application. The From:
26
+ # address can be either the email address of a registered administrator
27
+ # (developer) of the application, or the current user if signed in with
28
+ # Google Accounts.
29
+ #
30
+ # The following example sends an email message to the user as confirmation
31
+ # that the user created a new account with the application:
32
+ #
33
+ # class SignupController < Merb::Controller
34
+ # def confirm(self):
35
+ # user_address = params[:email_address"]
36
+ # confirmation_url = create_new_user_confirmation
37
+ # sender_address = "support@example.com"
38
+ # subject = "Confirm your registration"
39
+ # body = <<EOM
40
+ # Thank you for creating an account! Please confirm your email address by
41
+ # clicking on the link below:
42
+ #
43
+ # #{confirmation_url}
44
+ # EOM
45
+ #
46
+ # AppEngine::Mail.send(sender_address, user_address, subject, body)
47
+ # end
48
+ # end
49
+ module Mail
50
+ import com.google.appengine.api.mail.MailServiceFactory
51
+ import com.google.appengine.api.mail.MailService
52
+
53
+ module_function
54
+
55
+ # Sends an email.
56
+ #
57
+ # The message will be delivered asynchronously, and delivery problems
58
+ # will result in a bounce to the specified sender.
59
+ #
60
+ # Args:
61
+ # [sender] The From: field of the email. Must correspond to the valid
62
+ # email address of one of the admins for this application, or
63
+ # to the email address of the currently logged-in user.
64
+ # [to] Message recipient(s). Should be an email address, or an Array
65
+ # of email addresses.
66
+ # [subject] Subject of the message.
67
+ # [text] Plain text body of the message. To send an HTML only email,
68
+ # set +text+ to nil and use the +:html+ option.
69
+ # [options] See #create_java_message for supported options.
70
+ def send(sender, to, subject, text, options={})
71
+ orig_options = options
72
+ options = {
73
+ :sender => sender,
74
+ :to => to || [],
75
+ :subject => subject,
76
+ :text => text
77
+ }
78
+ options.merge!(orig_options)
79
+ message = create_java_message(options)
80
+ convert_mail_exceptions { service.send(message) }
81
+ end
82
+
83
+ # Sends an email alert to all admins of an application.
84
+ #
85
+ # The message will be delivered asynchronously, and delivery problems
86
+ # will result in a bounce to the admins.
87
+ #
88
+ # Args:
89
+ # [sender] The From: field of the email. Must correspond to the valid
90
+ # email address of one of the admins for this application, or
91
+ # to the email address of the currently logged-in user.
92
+ # [subject] Subject of the message.
93
+ # [text] Plain text body of the message. To send an HTML only email,
94
+ # set +text+ to nil and use the +:html+ option.
95
+ # [options] See #create_java_message for supported options.
96
+ def send_to_admins(sender, subject, text, options={})
97
+ orig_options = options
98
+ options = {
99
+ :sender => sender,
100
+ :subject => subject,
101
+ :text => text
102
+ }
103
+ options.merge!(orig_options)
104
+ message = create_java_message(options)
105
+ convert_mail_exceptions { service.send_to_admins(message) }
106
+ end
107
+
108
+ # Creates a Java MailService.Message object.
109
+ #
110
+ # Supported options:
111
+ # [:atttachments]
112
+ # Attachments to send with this message. Should be a Hash of
113
+ # {"filename" => "data"} or an Array of [["filename", "data"], ...].
114
+ # [:bcc] Must be a String or an Array of Strings if set.
115
+ # [:cc] Must be a String or an Array of Strings if set.
116
+ # [:html] The html body of the message. Must not be +nil+ if +text+ is nil.
117
+ # [:reply_to] Must be a valid email address if set.
118
+ def create_java_message(options)
119
+ options[:text_body] = options.delete(:text)
120
+ options[:html_body] = options.delete(:html)
121
+ attachments = options[:attachments]
122
+ if attachments
123
+ options[:attachments] = attachments.collect do |filename, data|
124
+ MailService::Attachment.new(filename, data.to_java_bytes)
125
+ end
126
+ end
127
+ [:to, :cc, :bcc].each do |key|
128
+ value = options[key]
129
+ options[key] = [value] if value.kind_of? String
130
+ end
131
+ message = MailService::Message.new
132
+ options.each do |key, value|
133
+ begin
134
+ message.send("set_#{key}", value) if value
135
+ rescue NameError
136
+ raise ArgumentError, "Invalid option #{key.inspect}."
137
+ end
138
+ end
139
+ return message
140
+ end
141
+
142
+ def convert_mail_exceptions # :nodoc:
143
+ begin
144
+ yield
145
+ rescue java.lang.IllegalArgumentException => ex
146
+ raise ArgumentError, ex.message
147
+ rescue java.io.IOException => ex
148
+ raise IOError, ex.message
149
+ end
150
+ end
151
+
152
+ def service # :nodoc:
153
+ @service ||= MailServiceFactory.mail_service
154
+ end
155
+
156
+ def service=(service) # :nodoc:
157
+ @service = service
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/ruby1.8 -w
2
+ #
3
+ # Copyright:: Copyright 2009 Google Inc.
4
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'appengine-apis/datastore_types'
20
+
21
+ module AppEngine
22
+
23
+ # The Ruby API for the App Engine Memcache service. This offers a fast
24
+ # distrubted cache for commonly-used data. The cache is limited both in
25
+ # duration and also in total space, so objects stored in it may be discarded
26
+ # at any time.
27
+ #
28
+ # Note that null is a legal value to store in the cache, or to use as a cache
29
+ # key. Strings are stored encoded as utf-8. To store binary data use
30
+ # AppEngine::Datastore::Blob or +str.to_java_bytes+.
31
+ #
32
+ # The values returned from this API are mutable copies from the cache;
33
+ # altering them has no effect upon the cached value itself until assigned
34
+ # with one of the put methods. Likewise, the methods returning collections
35
+ # return mutable collections, but changes do not affect the cache.
36
+ #
37
+ # Except for the #incr and #decr methods, this service does not offer
38
+ # atomicity guarantees. In particular, operations accessing multiple
39
+ # keys are non-atomic.
40
+ #
41
+ # Increment has a number of caveats to its use; please consult the method
42
+ # documentation.
43
+ class Memcache
44
+ import com.google.appengine.api.memcache.Expiration
45
+ import com.google.appengine.api.memcache.InvalidValueException
46
+ import com.google.appengine.api.memcache.LogAndContinueErrorHandler
47
+ import com.google.appengine.api.memcache.MemcacheServiceException
48
+ import com.google.appengine.api.memcache.MemcacheServiceFactory
49
+ import com.google.appengine.api.memcache.MemcacheService
50
+ import com.google.appengine.api.memcache.StrictErrorHandler
51
+
52
+ ADD = MemcacheService::SetPolicy::ADD_ONLY_IF_NOT_PRESENT
53
+ REPLACE = MemcacheService::SetPolicy::REPLACE_ONLY_IF_PRESENT
54
+ SET = MemcacheService::SetPolicy::SET_ALWAYS
55
+
56
+ # Base Memcache exception class
57
+ class MemcacheError < StandardError; end
58
+
59
+ MemCacheError = MemcacheError
60
+ ClientError = MemcacheError
61
+ InternalError = MemcacheError
62
+
63
+ MARSHAL_MARKER = '--JRuby Marshal Data--'
64
+
65
+ # An exception for backend non-availability or similar error states which
66
+ # may occur, but are not necessarily indicative of a coding or usage error
67
+ # by the application.
68
+ class ServerError < MemcacheError; end
69
+
70
+ # Raised when a cache entry has content, but it cannot be read. For example:
71
+ # - An attempt to #incr a non-integral value
72
+ # - Version skew between your application and the data in the cache,
73
+ # causing a marshaling error.
74
+ class InvalidValueError < MemcacheError; end
75
+
76
+ def initialize(*servers)
77
+ options = if servers[-1].kind_of? Hash
78
+ servers[-1]
79
+ else
80
+ {}
81
+ end
82
+ if options.include?(:namespace)
83
+ service.namespace = options[:namespace]
84
+ end
85
+ @readonly = options[:readonly]
86
+ end
87
+
88
+ # Returns the Java MemcacheService object used by this Memcache client.
89
+ def service
90
+ @service ||= MemcacheServiceFactory.memcache_service
91
+ end
92
+
93
+ def active?
94
+ # TODO use the capability api to see if Memcache is disabled.
95
+ true
96
+ end
97
+
98
+ # Empties the cache of all values. Statistics are not affected. Note that
99
+ # #clear does not respect namespaces - this flushes the cache for every
100
+ # namespace.
101
+ #
102
+ # Returns true on success, false on RPC or server error.
103
+ def flush_all
104
+ check_write
105
+ with_errors do
106
+ begin
107
+ service.clear_all
108
+ return true
109
+ rescue MemcacheError
110
+ return false
111
+ end
112
+ end
113
+ end
114
+ alias clear flush_all
115
+
116
+ # Gets memcache statistics for this application.
117
+ #
118
+ # All of these statistics may reset due to various transient conditions.
119
+ # They provide the best information available at the time of being called.
120
+ #
121
+ # Returns a Hash mapping statistic names to associated values:
122
+ # [:hits] Number of cache get requests resulting in a cache hit.
123
+ # [:misses] Number of cache get requests resulting in a cache miss.
124
+ # [:byte_hits] Sum of bytes transferred on get requests. Rolls over to
125
+ # zero on overflow.
126
+ # [:items] Number of key/value pairs in the cache.
127
+ # [:bytes] Total size of all items in the cache.
128
+ # [:oldest_item_age]
129
+ # How long in seconds since the oldest item in the
130
+ # cache was accessed. Effectively, this indicates how long a new
131
+ # item will survive in the cache without being accessed. This is
132
+ # _not_ the amount of time that has elapsed since the item was
133
+ # created.
134
+ #
135
+ # On error, returns +nil+.
136
+ def stats
137
+ with_errors do
138
+ begin
139
+ stats = service.statistics
140
+ if stats
141
+ {
142
+ :hits => stats.hit_count,
143
+ :misses => stats.miss_count,
144
+ :byte_hits => stats.bytes_returned_for_hits,
145
+ :items => stats.item_count,
146
+ :bytes => stats.total_item_bytes,
147
+ :oldest_item_age => stats.max_time_without_access / 1000.0
148
+ }
149
+ end
150
+ rescue ServerError
151
+ nil
152
+ end
153
+ end
154
+ end
155
+
156
+ # Fetch and return the values associated with the given +key+s from the
157
+ # cache. Returns +nil+ for any value that wasn’t in the cache.
158
+ def get(*keys)
159
+ multiple = (keys.size != 1)
160
+ if !multiple && keys[0].kind_of?(Array)
161
+ keys = keys[0]
162
+ multiple = true
163
+ end
164
+ hash = get_hash(*keys)
165
+ values = keys.collect {|key| hash[key]}
166
+ if multiple
167
+ values
168
+ else
169
+ values[0]
170
+ end
171
+ end
172
+ alias [] get
173
+
174
+ # Looks up multiple keys from memcache in one operation. This is more
175
+ # efficient than multiple separate calls to #get.
176
+ #
177
+ # Args:
178
+ # - keys: List of keys to look up.
179
+ #
180
+ # Returns a hash of the keys and values that were present in memcache.
181
+ def get_hash(*keys)
182
+ key_map = KeyMap.new(keys)
183
+ convert_exceptions do
184
+ map = service.getAll(key_map.java_keys)
185
+ key_map.map_to_hash(map) do |value|
186
+ if value.java_kind_of?(java.util.ArrayList) && value.size == 2 &&
187
+ value[0] == MARSHAL_MARKER
188
+ Marshal.load(String.from_java_bytes(value[1]))
189
+ else
190
+ value
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # Removes the given key from the cache, and prevents it from being added
197
+ # using #add for +time+ seconds thereafter. Calls to #set are not blocked.
198
+ #
199
+ # Returns true if an entry existed to delete.
200
+ def delete(key, time=nil)
201
+ time ||= 0
202
+ check_write
203
+ convert_exceptions do
204
+ service.delete(memcache_key(key), time * 1000)
205
+ end
206
+ end
207
+
208
+ # Removes the given keys from the cache, and prevents them from being added
209
+ # using #add for +time+ seconds thereafter. Calls to #set are not blocked.
210
+ #
211
+ # Returns the set of keys deleted. Any keys in +keys+ but not in the
212
+ # returned set were not found in the cache.
213
+ def delete_many(keys, time=0)
214
+ check_write
215
+ key_map = KeyMap.new(keys)
216
+ convert_exceptions do
217
+ java_keys = service.delete_all(key_map.java_keys, time * 1000)
218
+ key_map.ruby_keys(java_keys)
219
+ end
220
+ end
221
+
222
+ # Sets a key's value, iff item is not already in memcache.
223
+ #
224
+ # Args:
225
+ # - key: Key to set.
226
+ # - value: Value to set. Any type. If complex, will be marshaled.
227
+ # - expiration: Optional expiration time, either relative number of seconds
228
+ # from current time (up to 1 month), an absolute Unix epoch time, or a
229
+ # Time. By default, items never expire, though items may be evicted due
230
+ # to memory pressure.
231
+ #
232
+ # Returns true if added, false on error.
233
+ def add(key, value, expiration=0)
234
+ put(key, value, expiration, ADD)
235
+ end
236
+
237
+ # Set multiple keys' values iff items are not already in memcache.
238
+ #
239
+ # Args:
240
+ # - pairs: Hash of keys to values, or Array of [key, value] pairs.
241
+ # - expiration: Optional expiration time, either relative number of seconds
242
+ # from current time (up to 1 month), an absolute Unix epoch time, or a
243
+ # Time. By default, items never expire, though items may be evicted due
244
+ # to memory pressure.
245
+ #
246
+ # Returns a list of keys whose values were NOT set. On total success
247
+ # this list should be empty.
248
+ def add_many(pairs, expiration=0)
249
+ put_many(pairs, expiration, ADD)
250
+ end
251
+
252
+ # Sets a key's value, regardless of previous contents in cache.
253
+ #
254
+ # Unlike #add and #replace, this method always sets (or
255
+ # overwrites) the value in memcache, regardless of previous
256
+ # contents.
257
+ #
258
+ # Args:
259
+ # - key: Key to set.
260
+ # - value: Value to set. Any type. If complex, will be marshaled.
261
+ # - expiration: Optional expiration time, either relative number of seconds
262
+ # from current time (up to 1 month), an absolute Unix epoch time, or a
263
+ # Time. By default, items never expire, though items may be evicted due
264
+ # to memory pressure.
265
+ #
266
+ # Returns true if set, false on error.
267
+ def set(key, value, expiration=0)
268
+ put(key, value, expiration, SET)
269
+ end
270
+
271
+ # Set multiple keys' values at once, regardless of previous contents.
272
+ #
273
+ # Args:
274
+ # - pairs: Hash of keys to values, or Array of [key, value] pairs.
275
+ # - expiration: Optional expiration time, either relative number of seconds
276
+ # from current time (up to 1 month), an absolute Unix epoch time, or a
277
+ # Time. By default, items never expire, though items may be evicted due
278
+ # to memory pressure.
279
+ #
280
+ # Returns a list of keys whose values were NOT set. On total success
281
+ # this list should be empty.
282
+ def set_many(pairs, expiration=0)
283
+ put_many(pairs, expiration, SET)
284
+ end
285
+
286
+ # call-seq:
287
+ # cache[:foo, :bar] = 1, 2
288
+ #
289
+ # Syntactic sugar for calling set_many.
290
+ def []=(*args)
291
+ values = args.pop
292
+ if values.kind_of? Array
293
+ set_many(args.zip(values))
294
+ else
295
+ set(args, values)
296
+ end
297
+ end
298
+
299
+ # Replaces a key's value, failing if item isn't already in memcache.
300
+ #
301
+ # Unlike #add and #replace, this method always sets (or
302
+ # overwrites) the value in memcache, regardless of previous
303
+ # contents.
304
+ #
305
+ # Args:
306
+ # - key: Key to set.
307
+ # - value: Value to set. Any type. If complex, will be marshaled.
308
+ # - expiration: Optional expiration time, either relative number of seconds
309
+ # from current time (up to 1 month), an absolute Unix epoch time, or a
310
+ # Time. By default, items never expire, though items may be evicted due
311
+ # to memory pressure.
312
+ #
313
+ # Returns true if replaced, false on cache miss.
314
+ def replace(key, value, expiration=0)
315
+ put(key, value, expiration, REPLACE)
316
+ end
317
+
318
+ # Replace multiple keys' values, failing if the items aren't in memcache.
319
+ #
320
+ # Args:
321
+ # - pairs: Hash of keys to values, or Array of [key, value] pairs.
322
+ # - expiration: Optional expiration time, either relative number of seconds
323
+ # from current time (up to 1 month), an absolute Unix epoch time, or a
324
+ # Time. By default, items never expire, though items may be evicted due
325
+ # to memory pressure.
326
+ #
327
+ # Returns a list of keys whose values were NOT set. On total success
328
+ # this list should be empty.
329
+ def replace_many(pairs, expiration=0)
330
+ put_many(pairs, expiration, REPLACE)
331
+ end
332
+
333
+ # Atomically fetches, increments, and stores a given integral value.
334
+ # "Integral" types are Fixnum and in some cases String (if the string is
335
+ # parseable as a number. The entry must already exist.
336
+ #
337
+ # Internally, the value is a unsigned 64-bit integer. Memcache
338
+ # doesn't check 64-bit overflows. The value, if too large, will
339
+ # wrap around.
340
+ #
341
+ # Args:
342
+ # - key: the key of the entry to manipulate
343
+ # - delta: the size of the increment.
344
+ #
345
+ # Returns the post-increment value.
346
+ #
347
+ # Throws InvalidValueError if the object incremented is not of
348
+ # an integral type.
349
+ def incr(key, delta=1)
350
+ check_write
351
+ convert_exceptions do
352
+ service.increment(memcache_key(key), delta)
353
+ end
354
+ end
355
+
356
+ # Atomically fetches, deccrements, and stores a given integral value.
357
+ # "Integral" types are Fixnum and in some cases String (if the string is
358
+ # parseable as a number. The entry must already exist.
359
+ #
360
+ # Internally, the value is a unsigned 64-bit integer. Memcache
361
+ # caps decrementing below zero to zero.
362
+ #
363
+ # Args:
364
+ # - key: the key of the entry to manipulate
365
+ # - delta: the size of the decrement
366
+ #
367
+ # Returns the post-decrement value.
368
+ #
369
+ # Throws InvalidValueError if the object decremented is not of
370
+ # an integral type.
371
+ def decr(key, delta=1)
372
+ check_write
373
+ convert_exceptions do
374
+ service.increment(memcache_key(key), -delta)
375
+ end
376
+ end
377
+
378
+ # Get the name of the namespace that will be used in API calls.
379
+ def namespace
380
+ service.namespace
381
+ end
382
+
383
+ # Change the namespace used in API calls.
384
+ def namespace=(value)
385
+ service.namespace = value
386
+ end
387
+
388
+ # Returns true if the cache was created read-only.
389
+ def readonly?
390
+ @readonly
391
+ end
392
+
393
+ def inspect
394
+ "<Memcache ns:#{namespace.inspect}, ro:#{readonly?.inspect}>"
395
+ end
396
+
397
+ # Returns whether the client raises an exception if there's an error
398
+ # contacting the server. By default it will simulate a cache miss
399
+ # instead of raising an error.
400
+ def raise_errors?
401
+ service.error_handler.kind_of? StrictErrorHandler
402
+ end
403
+
404
+ # Set whether this client raises an exception if there's an error
405
+ # contacting the server.
406
+ #
407
+ # If +should_raise+ is true, a ServerError is raised whenever there
408
+ # is an error contacting the server.
409
+ #
410
+ # If +should_raise+ is false (the default), a cache miss is simulated
411
+ # instead of raising an error.
412
+ def raise_errors=(should_raise)
413
+ if should_raise
414
+ service.error_handler = StrictErrorHandler.new
415
+ else
416
+ service.error_handler = LogAndContinueErrorHandler.new
417
+ end
418
+ end
419
+
420
+ # For backwards compatibility. Simply returns nil
421
+ def do_nothing(*args)
422
+ end
423
+ alias server_item_stats do_nothing
424
+ alias server_malloc_stats do_nothing
425
+ alias server_map_stats do_nothing
426
+ alias server_reset_stats do_nothing
427
+ alias server_size_stats do_nothing
428
+ alias server_slab_stats do_nothing
429
+ alias server_stats do_nothing
430
+ alias servers= do_nothing
431
+
432
+ private
433
+
434
+ def memcache_key(obj)
435
+ key = obj
436
+ key = key.to_s.to_java_string if key
437
+ key
438
+ end
439
+
440
+ def memcache_value(obj)
441
+ case obj
442
+ when Fixnum
443
+ java.lang.Long.new(obj)
444
+ when Float
445
+ java.lang.Double.new(obj)
446
+ when TrueClass, FalseClass
447
+ java.lang.Boolean.new(obj)
448
+ when JavaProxy, Java::JavaObject
449
+ obj
450
+ else
451
+ if obj.class == String
452
+ # Convert plain strings to Java strings
453
+ obj.to_java_string
454
+ else
455
+ bytes = Marshal.dump(obj).to_java_bytes
456
+ java.util.ArrayList.new([MARSHAL_MARKER.to_java_string, bytes])
457
+ end
458
+ end
459
+ end
460
+
461
+ def memcache_expiration(amount)
462
+ if amount.nil? || amount == 0
463
+ nil
464
+ elsif amount.kind_of? Time
465
+ Expiration.on_date(amount.to_java)
466
+ elsif amount > 86400 * 30
467
+ millis = (amount * 1000).to_i
468
+ Expiration.on_date(java.util.Date.new(millis))
469
+ else
470
+ Expiration.byDeltaMillis((amount * 1000).to_i)
471
+ end
472
+ end
473
+
474
+ def check_write
475
+ raise MemcacheError, "readonly cache" if self.readonly?
476
+ end
477
+
478
+ def with_errors(&block)
479
+ saved_handler = service.error_handler
480
+ begin
481
+ service.error_handler = StrictErrorHandler.new
482
+ convert_exceptions(&block)
483
+ ensure
484
+ service.error_handler = saved_handler
485
+ end
486
+ end
487
+
488
+ def convert_exceptions
489
+ begin
490
+ yield
491
+ rescue java.lang.IllegalArgumentException => ex
492
+ raise ArgumentError, ex.message
493
+ rescue InvalidValueException => ex
494
+ raise InvalidValueError, ex.message
495
+ rescue MemcacheServiceException => ex
496
+ raise ServerError, ex.message
497
+ end
498
+ end
499
+
500
+ def put(key, value, expiration, mode)
501
+ check_write
502
+ convert_exceptions do
503
+ key = memcache_key(key)
504
+ value = memcache_value(value)
505
+ expiriation = memcache_expiration(expiriation)
506
+ service.put(key, value, expiriation, mode)
507
+ end
508
+ end
509
+
510
+ def put_many(pairs, expiration, mode)
511
+ check_write
512
+ expiration = memcache_expiration(expiration)
513
+ convert_exceptions do
514
+ key_map = KeyMap.new
515
+ put_map = java.util.HashMap.new
516
+ pairs.each do |key, value|
517
+ java_key = key_map << key
518
+ java_value = memcache_value(value)
519
+ put_map.put(java_key, java_value)
520
+ end
521
+ saved_keys = service.put_all(put_map, expiration, mode)
522
+ key_map.missing_keys(saved_keys)
523
+ end
524
+ end
525
+
526
+ class KeyMap # :nodoc:
527
+ def initialize(keys=[])
528
+ @orig_keys = []
529
+ @map = {}
530
+ keys.each do |key|
531
+ self << key
532
+ end
533
+ end
534
+
535
+ def <<(key)
536
+ @orig_keys << key
537
+ string_key = if key
538
+ key.to_s
539
+ else
540
+ key
541
+ end
542
+ @map[string_key] = key
543
+ if string_key
544
+ string_key.to_java_string
545
+ else
546
+ string_key
547
+ end
548
+ end
549
+
550
+ def java_keys
551
+ @map.keys.collect do |key|
552
+ if key
553
+ key.to_java_string
554
+ else
555
+ key
556
+ end
557
+ end
558
+ end
559
+
560
+ def ruby_keys(keys)
561
+ keys.collect {|key| @map[key]}
562
+ end
563
+
564
+ def missing_keys(keys)
565
+ @orig_keys - ruby_keys(keys)
566
+ end
567
+
568
+ def map_to_hash(java_map)
569
+ hash = {}
570
+ if java_map
571
+ java_map.each do |key, value|
572
+ value = yield(value)
573
+ hash[@map[key]] = value
574
+ end
575
+ end
576
+ hash
577
+ end
578
+ end
579
+ end
580
+ end