/* * Copyright 2004-2010 the Seasar Foundation and the Others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.slim3.memcache; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import com.google.appengine.api.NamespaceManager; import com.google.appengine.api.memcache.ErrorHandler; import com.google.appengine.api.memcache.Expiration; import com.google.appengine.api.memcache.InvalidValueException; import com.google.appengine.api.memcache.MemcacheSerialization; import com.google.appengine.api.memcache.MemcacheService; import com.google.appengine.api.memcache.MemcacheServiceException; import com.google.appengine.api.memcache.MemcacheServiceFactory; import com.google.appengine.api.memcache.MemcacheServicePb; import com.google.appengine.api.memcache.Stats; import com.google.appengine.api.memcache.MemcacheService.SetPolicy; import com.google.appengine.repackaged.com.google.protobuf.ByteString; import com.google.appengine.repackaged.com.google.protobuf.InvalidProtocolBufferException; import com.google.appengine.repackaged.com.google.protobuf.Message; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.CapabilityDisabledException; /** * A delegate to access memcache service. * * @author higa * @since 1.0.0 * */ public class MemcacheDelegate { private static final Logger logger = Logger.getLogger(MemcacheDelegate.class.getName()); /** * The memcache service. */ protected MemcacheService ms; /** * Constructor. */ public MemcacheDelegate() { ms = MemcacheServiceFactory.getMemcacheService(); ms.setErrorHandler(new S3ErrorHandler()); } /** * Empties the cache of all values. Statistics are not affected. Note that * clearAll() does not respect namespaces - this flushes the cache for every * namespace. * * @throws CapabilityDisabledException * if memcache service is disable */ public void cleanAll() throws CapabilityDisabledException { ms.clearAll(); } /** * Tests whether a given value is in cache, even if its value is null. * * @param key * the key * @return whether a given value is in cache * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public boolean contains(Object key) throws IllegalArgumentException, CapabilityDisabledException { return ms.contains(key); } /** * Removes key from the cache. * * @param key * the key * @return true if an entry existed to delete * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public boolean delete(Object key) throws IllegalArgumentException, CapabilityDisabledException { return ms.delete(key); } /** * Removes the given key from the cache, and prevents it from being added * under the MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT policy for * millisNoReAdd milliseconds thereafter. Calls to a put(java.lang.Object, * java.lang.Object, com.google.appengine.api.memcache.Expiration, * com.google.appengine.api.memcache.MemcacheService.SetPolicy) method using * MemcacheService.SetPolicy.SET_ALWAYS are not blocked, however. * * @param key * the key * @param millisNoReAdd * the time during which calls to put using ADD_IF_NOT_PRESENT * should be denied * @return true if an entry existed to delete * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public boolean delete(Object key, long millisNoReAdd) throws IllegalArgumentException, CapabilityDisabledException { return ms.delete(key, millisNoReAdd); } /** * Removes keys from the cache. * * @param keys * the keys for entries to delete. * @return the Set of keys deleted. Any keys in keys but not in the returned * set were not found in the cache. * @throws NullPointerException * the keys parameter is null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable * */ public Set<Object> deleteAll(Iterable<?> keys) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { Collection<Object> keys2 = toCollection(keys); return ms.deleteAll(keys2); } /** * Removes keys from the cache. * * @param keys * the keys for entries to delete. * @param millisNoReAdd * the time during which calls to put using ADD_IF_NOT_PRESENT * should be denied * @return the Set of keys deleted. Any keys in keys but not in the returned * set were not found in the cache. * @throws NullPointerException * the keys parameter is null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable * */ public Set<Object> deleteAll(Iterable<?> keys, long millisNoReAdd) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { Collection<Object> keys2 = toCollection(keys); return ms.deleteAll(keys2, millisNoReAdd); } /** * Returns a previously-stored value, or null if unset. Since null might be * the set value in some cases, so we also have contains(Object) which * returns boolean. * * @param key * the key * @return a previously-stored value * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Object get(Object key) throws IllegalArgumentException, CapabilityDisabledException { return getInternal(key); } /** * Returns a previously-stored value, or null if unset. Since null might be * the set value in some cases, so we also have contains(Object) which * returns boolean. * * @param key * the key * @return a previously-stored value * @throws IllegalArgumentException * if the key cannot be serialized */ protected Object getInternal(Object key) { MemcacheServicePb.MemcacheGetResponse.Builder response = MemcacheServicePb.MemcacheGetResponse.newBuilder(); MemcacheServicePb.MemcacheGetRequest request; try { request = MemcacheServicePb.MemcacheGetRequest .newBuilder() .setNameSpace(getNamespace()) .addKey( ByteString.copyFrom(MemcacheSerialization .makePbKey(key))) .build(); } catch (IOException ex) { throw new IllegalArgumentException( (new StringBuilder()).append("Cannot use as a key: '").append( key).append("'").toString(), ex); } if (!makeSyncCall("Get", request, response, (new StringBuilder()) .append("Memcache get: exception getting 1 key (") .append(key) .append(")") .toString())) return null; if (response.getItemCount() == 0) return null; MemcacheServicePb.MemcacheGetResponse.Item item = response.getItem(0); try { return S3MemcacheSerialization.deserialize(item .getValue() .toByteArray(), item.getFlags()); } catch (ClassNotFoundException ex) { ms.getErrorHandler().handleDeserializationError( new InvalidValueException((new StringBuilder()).append( "Can't find class for value of key '").append(key).append( "'").toString(), ex)); } catch (IOException ex) { throw new InvalidValueException((new StringBuilder()) .append("IO exception parsing value of '") .append(key) .append("'") .toString(), ex); } return null; } /** * Returns the namespace. * * @return the namespace */ protected String getNamespace() { String namespace = NamespaceManager.get(); return namespace != null ? namespace : ""; } /** * Performs a get of multiple keys at once. This is more efficient than * multiple separate calls to get(Object), and allows a single call to both * test for contains(Object) and also fetch the value, because the return * will not include mappings for keys not found. * * @param keys * the keys. * @return a mapping from keys to values of any entries found. If a * requested key is not found in the cache, the key will not be in * the returned Map. * @throws NullPointerException * if the keys parameter is null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Map<Object, Object> getAll(Iterable<?> keys) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { return getAllInternal(keys); } /** * Performs a get of multiple keys at once. This is more efficient than * multiple separate calls to get(Object), and allows a single call to both * test for contains(Object) and also fetch the value, because the return * will not include mappings for keys not found. * * @param keys * the keys. * @return a mapping from keys to values of any entries found. If a * requested key is not found in the cache, the key will not be in * the returned Map. * @throws NullPointerException * if the keys parameter is null * @throws IllegalArgumentException * if the key cannot be serialized */ @SuppressWarnings("unchecked") protected Map<Object, Object> getAllInternal(Iterable<?> keys) throws NullPointerException, IllegalArgumentException { if (keys == null) { throw new NullPointerException( "The keys parameter must not be null."); } MemcacheServicePb.MemcacheGetResponse.Builder response = MemcacheServicePb.MemcacheGetResponse.newBuilder(); MemcacheServicePb.MemcacheGetRequest.Builder requestBuilder = MemcacheServicePb.MemcacheGetRequest.newBuilder().setNameSpace( getNamespace()); Map<CacheKey, Object> cacheKeyToObjectKey = new HashMap<CacheKey, Object>(); for (Object key : keys) { try { byte keybytes[] = MemcacheSerialization.makePbKey(key); cacheKeyToObjectKey.put(new CacheKey(keybytes), key); requestBuilder.addKey(ByteString.copyFrom(keybytes)); } catch (IOException ex) { throw new IllegalArgumentException((new StringBuilder()) .append("Cannot use as key: '") .append(key) .append("'") .toString(), ex); } } if (!makeSyncCall( "Get", requestBuilder.build(), response, "Memcache get: exception getting multiple keys")) return Collections.emptyMap(); Map result = new HashMap(); Iterator i = response.getItemList().iterator(); do { if (!i.hasNext()) { break; } MemcacheServicePb.MemcacheGetResponse.Item item = (MemcacheServicePb.MemcacheGetResponse.Item) i.next(); Object key = null; try { key = cacheKeyToObjectKey.get(new CacheKey(item .getKey() .toByteArray())); Object obj = S3MemcacheSerialization.deserialize(item .getValue() .toByteArray(), item.getFlags()); result.put(key, obj); } catch (ClassNotFoundException ex) { ms.getErrorHandler().handleDeserializationError( new InvalidValueException((new StringBuilder()) .append("Can't find class for value of key '") .append(key) .append("'") .toString(), ex)); return null; } catch (IOException ex) { throw new InvalidValueException((new StringBuilder()) .append("IO exception parsing value of '") .append(key) .append("'") .toString(), ex); } } while (true); return result; } /** * Atomically fetches, increments, and stores a given integral value. * "Integral" types are Byte, Short, Integer, Long, and in some cases String * (if the string is parseable as a number, for example via * Long.parseLong(String). The entry must already exist, and have a * non-negative value. * * Incrementing by positive amounts will reach signed 64-bit max (2^63 - 1) * and then wrap-around to signed 64-bit min (-2^63), continuing increments * from that point. * * To facilitate use as an atomic countdown, incrementing by a negative * value (i.e. decrementing) will not go below zero: incrementing 2 by -5 * will return 0, not -3. However, due to the way numbers are stored, * decrementing -3 by -5 will result in -8; so the zero-floor rule only * applies to decrementing numbers that were positive. * * Note: The actual representation of all numbers in Memcache is a string. * This means if you initially stored a number as a string (e.g., "10") and * then increment it, everything will work properly, including wrapping * beyond signed 64-bit int max. However, if you get the key past the point * of wrapping, you will receive an unsigned integer value, not a signed * integer value. * * @param key * the key * @param delta * the size of the increment, positive or negative. * @return the post-increment value, as a long. However, a get(Object) of * the key will still have the original type (Byte, Short, etc.). If * there is no entry for key, returns null. * @throws IllegalArgumentException * if the key cannot be serialized * @throws InvalidValueException * if the object incremented is not of a integral type * @throws CapabilityDisabledException * if memcache service is disable */ public Long increment(Object key, long delta) throws IllegalArgumentException, InvalidValueException, CapabilityDisabledException { return ms.increment(key, delta); } /** * Like normal increment, but allows for an optional initial value for the * key to take on if not already present in the cache. * * @param key * the key * @param delta * the size of the increment, positive or negative. * @param initialValue * the value to insert into the cache if the key is not present * @return the post-increment value, as a long. However, a get(Object) of * the key will still have the original type (Byte, Short, etc.). If * there is no entry for key, returns null. * @throws IllegalArgumentException * if the key cannot be serialized * @throws InvalidValueException * if the object incremented is not of a integral type * @throws CapabilityDisabledException * if memcache service is disable */ public Long increment(Object key, long delta, long initialValue) throws IllegalArgumentException, InvalidValueException, CapabilityDisabledException { return ms.increment(key, delta, initialValue); } /** * Like normal increment, but increments a batch of separate keys in * parallel by the same delta. * * @param keys * the keys * @param delta * the size of the increment, positive or negative. * @return mapping keys to their new values; values will be null if they * could not be incremented or were not present in the cache * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Map<Object, Long> incrementAll(Iterable<?> keys, long delta) throws IllegalArgumentException, CapabilityDisabledException { return ms.incrementAll(toCollection(keys), delta); } /** * Like normal increment, but increments a batch of separate keys in * parallel by the same delta and potentially sets a starting value. * * @param keys * the keys * @param delta * the size of the increment, positive or negative. * @param initialValue * the value to insert into the cache if the key is not present * @return mapping keys to their new values; values will be null if they * could not be incremented for whatever reason * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Map<Object, Long> incrementAll(Iterable<?> keys, long delta, long initialValue) throws IllegalArgumentException, CapabilityDisabledException { return ms.incrementAll(toCollection(keys), delta, initialValue); } /** * Like normal increment, but accepts a mapping of separate controllable * offsets for each key individually. * * @param offsets * the offsets * * @return mapping keys to their new values; values will be null if they * could not be incremented for whatever reason * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Map<Object, Long> incrementAll(Map<Object, Long> offsets) throws IllegalArgumentException, CapabilityDisabledException { return ms.incrementAll(offsets); } /** * Like normal increment, but accepts a mapping of separate controllable * offsets for each key individually. Callers may also pass an initial value * for the keys to take on if they are not already present in the cache. * * @param offsets * the offsets * @param initialValue * the value to insert into the cache if the key is not present * @return mapping keys to their new values; values will be null if they * could not be incremented for whatever reason * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Map<Object, Long> incrementAll(Map<Object, Long> offsets, long initialValue) throws IllegalArgumentException, CapabilityDisabledException { return ms.incrementAll(offsets); } /** * A convenience shortcut, equivalent to put(key, value, null, * SetPolicy.SET_ALWAYS). * * @param key * the key * @param value * the value * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public void put(Object key, Object value) throws IllegalArgumentException, CapabilityDisabledException { ms.put(key, value); } /** * A convenience shortcut, equivalent to put(key, value, expires, * SetPolicy.SET_ALWAYS). * * @param key * the key * @param value * the value * @param expires * expiration time for the new values, or null for no time-based * expiration * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public void put(Object key, Object value, Expiration expires) throws IllegalArgumentException, CapabilityDisabledException { ms.put(key, value, expires); } /** * Store a new value into the cache, using key, but subject to the policy * regarding existing entries. * * @param key * the key * @param value * the value * @param expires * expiration time for the new values, or null for no time-based * expiration * @param policy * Requests particular handling regarding pre-existing entries * under the same key. This parameter must not be null. * @return true if a new entry was created, false if not because of the * policy. * @throws NullPointerException * if the policy parameter is null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public boolean put(Object key, Object value, Expiration expires, SetPolicy policy) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { if (policy == null) { throw new NullPointerException( "The policy parameter must not be null."); } return ms.put(key, value, expires, policy); } /** * A convenience shortcut, equivalent to putAll(values, null, * SetPolicy.SET_ALWAYS). * * @param values * key/value mappings for new entries to add to the cache * @throws NullPointerException * if the values parameter is null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public void putAll(Map<Object, Object> values) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { if (values == null) { throw new NullPointerException( "The values parameter must not be null."); } ms.putAll(values); } /** * A convenience shortcut, equivalent to putAll(values, expires, * SetPolicy.SET_ALWAYS). * * @param values * key/value mappings for new entries to add to the cache * @param expires * expiration time for the new values, or null for no time-based * expiration * @throws NullPointerException * if the values parameter is null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public void putAll(Map<Object, Object> values, Expiration expires) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { if (values == null) { throw new NullPointerException( "The values parameter must not be null."); } ms.putAll(values, expires); } /** * A batch-processing variant of put(java.lang.Object, java.lang.Object, * com.google.appengine.api.memcache.Expiration, * com.google.appengine.api.memcache.MemcacheService.SetPolicy). This is * more efficiently implemented by the service than multiple calls. * * @param values * key/value mappings for new entries to add to the cache * @param expires * expiration time for the new values, or null for no time-based * expiration * @param policy * what to do if the entry is or is not already present * @return the set of keys for which entries were created. Keys in values * may not be in the returned set because of the policy regarding * pre-existing entries. * @throws NullPointerException * if the values parameter is null or if the policy parameter is * null * @throws IllegalArgumentException * if the key cannot be serialized * @throws CapabilityDisabledException * if memcache service is disable */ public Set<Object> putAll(Map<Object, Object> values, Expiration expires, SetPolicy policy) throws NullPointerException, IllegalArgumentException, CapabilityDisabledException { if (values == null) { throw new NullPointerException( "The values parameter must not be null."); } if (policy == null) { throw new NullPointerException( "The policy parameter must not be null."); } return ms.putAll(values, expires, policy); } /** * Returns the current error handler. * * @return the current error handler. */ public ErrorHandler errorHandler() { return ms.getErrorHandler(); } /** * Sets the current error handler. * * @param errorHandler * the error handler * @return this instance * @throws NullPointerException * if the errorHandler parameter is null */ public MemcacheDelegate errorHandler(ErrorHandler errorHandler) throws NullPointerException { if (errorHandler == null) { throw new NullPointerException( "The errorHandler parameter must not be null."); } ms.setErrorHandler(errorHandler); return this; } /** * Returns some statistics about the cache and its usage. Note that * statistics() does not respect namespaces - this will return stats for * every namespace. The response will never be null. * * @return some statistics */ public Stats statistics() { return ms.getStatistics(); } /** * Makes a sync call. * * @param methodName * the method name. * @param request * the request. * @param response * the response. * @param errorText * the error text. * @return whether this call succeeded */ protected boolean makeSyncCall(String methodName, Message request, Message.Builder response, String errorText) { try { byte responseBytes[] = ApiProxy.makeSyncCall("memcache", methodName, request .toByteArray()); response.mergeFrom(responseBytes); return true; } catch (InvalidProtocolBufferException ex) { ms.getErrorHandler().handleServiceError( new MemcacheServiceException("Could not decode response:", ex)); } catch (com.google.apphosting.api.ApiProxy.ApplicationException ae) { logger.info((new StringBuilder()) .append(errorText) .append(": ") .append(ae.getErrorDetail()) .toString()); ms.getErrorHandler().handleServiceError( new MemcacheServiceException(errorText)); } catch (com.google.apphosting.api.ApiProxy.ApiProxyException ex) { ms.getErrorHandler().handleServiceError( new MemcacheServiceException(errorText, ex)); } return false; } /** * Converts the keys to a collection of keys. * * @param keys * the keys. * @return a collection of the keys. * @throws NullPointerException * if the keys parameter is null */ @SuppressWarnings("unchecked") protected Collection<Object> toCollection(Iterable<?> keys) throws NullPointerException { if (keys == null) { throw new NullPointerException( "The keys parameter must not be null."); } if (keys instanceof Collection<?>) { return (Collection<Object>) keys; } Collection<Object> ret = new ArrayList<Object>(); for (Object key : keys) { ret.add(key); } return ret; } /** * A key class for an array of bytes. * */ protected static class CacheKey { /** * The array of bytes. */ protected byte bytes[]; /** * The cached hash code. */ protected int hashCode; /** * Constructor. * * @param bytes * the array of bytes */ public CacheKey(byte bytes[]) { this.bytes = bytes; hashCode = Arrays.hashCode(bytes); } /** * Returns the array of bytes. * * @return the array of bytes */ public byte[] getBytes() { return bytes; } @Override public boolean equals(Object other) { if (other instanceof CacheKey) { return Arrays.equals(bytes, ((CacheKey) other).bytes); } return false; } @Override public int hashCode() { return hashCode; } } }