appengine-apis 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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