/* * Copyright (c) 2013 Google Inc. * * 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 com.google.api.client.extensions.appengine.datastore; import com.google.api.client.util.IOUtils; import com.google.api.client.util.Lists; import com.google.api.client.util.Maps; import com.google.api.client.util.Preconditions; import com.google.api.client.util.Sets; import com.google.api.client.util.store.AbstractDataStore; import com.google.api.client.util.store.AbstractDataStoreFactory; import com.google.api.client.util.store.DataStore; import com.google.api.client.util.store.DataStoreUtils; import com.google.appengine.api.datastore.Blob; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.EntityNotFoundException; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.memcache.Expiration; import com.google.appengine.api.memcache.MemcacheService; import com.google.appengine.api.memcache.MemcacheServiceFactory; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Thread-safe Google App Engine implementation of a data store factory that directly uses the App * Engine Data Store API. * * <p> * For convenience, a default global instance is provided in {@link #getDefaultInstance()}. * </p> * * <p> * By default, it uses the Memcache API as an in-memory data cache. To disable it, call * {@link Builder#setDisableMemcache(boolean)}. The Memcache is only read to check if a key already * has a value inside {@link DataStore#get(String)}. The values in the Memcache are updated in the * {@link DataStore#get(String)}, {@link DataStore#set(String, Serializable)}, * {@link DataStore#delete(String)}, {@link DataStore#values()}, and {@link DataStore#clear()} * methods. * </p> * * @since 1.16 * @author Yaniv Inbar */ public class AppEngineDataStoreFactory extends AbstractDataStoreFactory { /** Whether to disable the memcache (which is enabled by default). */ final boolean disableMemcache; /** Memcache expiration policy on puts. */ final Expiration memcacheExpiration; @Override protected <V extends Serializable> DataStore<V> createDataStore(String id) throws IOException { return new AppEngineDataStore<V>(this, id); } public AppEngineDataStoreFactory() { this(new Builder()); } /** * @param builder builder */ public AppEngineDataStoreFactory(Builder builder) { disableMemcache = builder.disableMemcache; memcacheExpiration = builder.memcacheExpiration; } /** Returns whether to disable the memcache (which is enabled by default). */ public boolean getDisableMemcache() { return disableMemcache; } /** * Returns a global thread-safe instance based on the default constructor * {@link #AppEngineDataStoreFactory()}. */ public static AppEngineDataStoreFactory getDefaultInstance() { return InstanceHolder.INSTANCE; } /** Holder for the result of {@link #getDefaultInstance()}. */ static class InstanceHolder { static final AppEngineDataStoreFactory INSTANCE = new AppEngineDataStoreFactory(); } static class AppEngineDataStore<V extends Serializable> extends AbstractDataStore<V> { /** Lock on access to the store. */ private final Lock lock = new ReentrantLock(); /** Name of the field in which the value is stored. */ private static final String FIELD_VALUE = "value"; /** The service instance used to access the Memcache API. */ private final MemcacheService memcache; /** Data store service. */ private final DatastoreService dataStoreService; /** Memcache expiration policy on puts. */ final Expiration memcacheExpiration; AppEngineDataStore(AppEngineDataStoreFactory dataStoreFactory, String id) { super(dataStoreFactory, id); memcache = dataStoreFactory.disableMemcache ? null : MemcacheServiceFactory.getMemcacheService(id); memcacheExpiration = dataStoreFactory.memcacheExpiration; dataStoreService = DatastoreServiceFactory.getDatastoreService(); } /** Deserializes the specified object from a Blob using an {@link ObjectInputStream}. */ private V deserialize(Entity entity) throws IOException { Blob blob = (Blob) entity.getProperty(FIELD_VALUE); return IOUtils.deserialize(blob.getBytes()); } @Override public Set<String> keySet() throws IOException { lock.lock(); try { // NOTE: not possible with memcache Set<String> result = Sets.newHashSet(); for (Entity entity : query(true)) { result.add(entity.getKey().getName()); } return Collections.unmodifiableSet(result); } finally { lock.unlock(); } } @Override public Collection<V> values() throws IOException { lock.lock(); try { // Unfortunately no getKeys() method on MemcacheService, so the only option is to clear all // and re-populate the memcache from scratch. This is clearly inefficient. if (memcache != null) { memcache.clearAll(); } List<V> result = Lists.newArrayList(); Map<String, V> map = memcache != null ? Maps.<String, V>newHashMap() : null; for (Entity entity : query(false)) { V value = deserialize(entity); result.add(value); if (map != null) { map.put(entity.getKey().getName(), value); } } if (memcache != null) { memcache.putAll(map, memcacheExpiration); } return Collections.unmodifiableList(result); } finally { lock.unlock(); } } @Override public V get(String key) throws IOException { if (key == null) { return null; } lock.lock(); try { if (memcache != null && memcache.contains(key)) { @SuppressWarnings("unchecked") V result = (V) memcache.get(key); return result; } Key dataKey = KeyFactory.createKey(getId(), key); Entity entity; try { entity = dataStoreService.get(dataKey); } catch (EntityNotFoundException exception) { if (memcache != null) { memcache.delete(key); } return null; } V result = deserialize(entity); if (memcache != null) { memcache.put(key, result, memcacheExpiration); } return result; } finally { lock.unlock(); } } @Override public AppEngineDataStore<V> set(String key, V value) throws IOException { Preconditions.checkNotNull(key); Preconditions.checkNotNull(value); lock.lock(); try { Entity entity = new Entity(getId(), key); entity.setUnindexedProperty(FIELD_VALUE, new Blob(IOUtils.serialize(value))); dataStoreService.put(entity); if (memcache != null) { memcache.put(key, value, memcacheExpiration); } } finally { lock.unlock(); } return this; } @Override public DataStore<V> delete(String key) throws IOException { if (key == null) { return this; } lock.lock(); try { dataStoreService.delete(KeyFactory.createKey(getId(), key)); if (memcache != null) { memcache.delete(key); } } finally { lock.unlock(); } return this; } @Override public AppEngineDataStore<V> clear() throws IOException { lock.lock(); try { if (memcache != null) { memcache.clearAll(); } // no clearAll() method on DataStoreService so have to query all keys & delete them List<Key> keys = Lists.newArrayList(); for (Entity entity : query(true)) { keys.add(entity.getKey()); } dataStoreService.delete(keys); } finally { lock.unlock(); } return this; } @Override public AppEngineDataStoreFactory getDataStoreFactory() { return (AppEngineDataStoreFactory) super.getDataStoreFactory(); } @Override public String toString() { return DataStoreUtils.toString(this); } /** * Query on all of the keys for the data store ID. * * @param keysOnly whether to call {@link Query#setKeysOnly()} * @return iterable over the entities in the query result */ private Iterable<Entity> query(boolean keysOnly) { Query query = new Query(getId()); if (keysOnly) { query.setKeysOnly(); } return dataStoreService.prepare(query).asIterable(); } } /** * App Engine data store factory builder. * * <p> * Implementation is not thread-safe. * </p> * * @since 1.16 */ public static class Builder { /** Whether to disable the memcache. */ boolean disableMemcache; /** Memcache expiration policy on puts. */ Expiration memcacheExpiration; /** Returns whether to disable the memcache. */ public final boolean getDisableMemcache() { return disableMemcache; } /** * Sets whether to disable the memcache ({@code false} by default). * * <p> * Overriding is only supported for the purpose of calling the super implementation and changing * the return type, but nothing else. * </p> */ public Builder setDisableMemcache(boolean disableMemcache) { this.disableMemcache = disableMemcache; return this; } /** Returns the Memcache expiration policy on puts. */ public final Expiration getMemcacheExpiration() { return memcacheExpiration; } /** * Sets the Memcache expiration policy on puts. * * <p> * Overriding is only supported for the purpose of calling the super implementation and changing * the return type, but nothing else. * </p> */ public Builder setMemcacheExpiration(Expiration memcacheExpiration) { this.memcacheExpiration = memcacheExpiration; return this; } /** Returns a new App Engine data store factory instance. */ public AppEngineDataStoreFactory build() { return new AppEngineDataStoreFactory(); } } }