All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tectonica.gae.GaeKeyValueStore Maven / Gradle / Ivy

package com.tectonica.gae;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;

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.datastore.Query.Filter;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.tectonica.collections.KeyValueStore;
import com.tectonica.util.KryoUtil;
import com.tectonica.util.SerializeUtil;

public class GaeKeyValueStore extends KeyValueStore
{
	private static DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

	private final Class valueClass;
	private final String kind;
	private final Key ancestor; // dummy parent for all entities to guarantee Datastore consistency
	private final Serializer serializer;
	private final List> indexes;

	public GaeKeyValueStore(Class valueClass, KeyMapper keyMapper)
	{
		this(valueClass, keyMapper, new JavaSerializer());
	}

	public GaeKeyValueStore(Class valueClass, KeyMapper keyMapper, Serializer serializer)
	{
		super(keyMapper);
		if (valueClass == null)
			throw new NullPointerException("valueClass");
		if (serializer == null)
			throw new NullPointerException("serializer");
		this.valueClass = valueClass;
		this.kind = valueClass.getSimpleName();
		this.ancestor = KeyFactory.createKey(kind, BOGUS_ANCESTOR_KEY_NAME);
		this.indexes = new ArrayList<>();
		this.serializer = serializer;
	}

	private Key keyOf(String key)
	{
		return KeyFactory.createKey(ancestor, kind, key);
	}

	@Override
	protected Cache createCache()
	{
		if (serializer instanceof JavaSerializer)
			return new JavaSerializeCache();
		return new CustomSerializeCache();
	}

	/***********************************************************************************
	 * 
	 * GETTERS
	 * 
	 ***********************************************************************************/

	@Override
	protected V dbGet(String key)
	{
		try
		{
			return entityToValue(ds.get(keyOf(key)));
		}
		catch (EntityNotFoundException e)
		{
			return null;
		}
	}

	@Override
	public Iterator> iterator()
	{
		return entryIteratorOfQuery(newQuery()); // query without filters = all
	}

	@Override
	public Iterator keyIterator()
	{
		return keyIteratorOfQuery(newQuery().setKeysOnly());
	}

	@Override
	public Iterator valueIterator()
	{
		return valueIteratorOfQuery(newQuery());
	}

	@Override
	protected Iterator> dbOrderedIterator(Collection keys)
	{
		if (keys.size() > 30)
			throw new RuntimeException("GAE doesn't support more than 30 at the time, need to break it");

		List gaeKeys = new ArrayList<>(keys.size());
		for (String key : keys)
			gaeKeys.add(keyOf(key));

		// we define a filter based on the IN operator, which returns values in the order of listing.
		// see: https://cloud.google.com/appengine/docs/java/datastore/queries#Java_Query_structure
		Filter filter = new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.IN, gaeKeys);
		return entryIteratorOfQuery(newQuery().setFilter(filter));
	}

	/***********************************************************************************
	 * 
	 * SETTERS (UTILS)
	 * 
	 ***********************************************************************************/

	@Override
	protected Modifier getModifier(final String key, ModificationType purpose)
	{
		// insert, replace and update are all treated the same in GAE: all simply do save()
		return new Modifier()
		{
			@Override
			public V getModifiableValue()
			{
				// we use here same calls as if for read-only value, because in both cases a new instance is deserialized
				// NOTE: if we ever switch to a different implementation, with local objects, this wouldn't work
				return get(key, false);
			}

			@Override
			public void dbPut(V value)
			{
				save(key, value);
			}
		};
	}

	@Override
	public Lock getModificationLock(String key)
	{
		return GaeMemcacheLock.getLock(kind + ":" + key, true);
	}

	/***********************************************************************************
	 * 
	 * SETTERS
	 * 
	 ***********************************************************************************/

	@Override
	protected void dbInsert(String key, V value)
	{
		save(key, value);
	}

	/***********************************************************************************
	 * 
	 * DELETERS
	 * 
	 ***********************************************************************************/

	@Override
	protected boolean dbDelete(String key)
	{
		ds.delete(keyOf(key));
		return true; // we don't really know if the key previously existed
	}

	@Override
	protected int dbDeleteAll()
	{
		int removed = 0;
		for (Entity entity : ds.prepare(newQuery().setKeysOnly()).asIterable())
		{
			ds.delete(entity.getKey());
			removed++;
		}
		return removed; // an estimate. we have to assume that all keys existed before delete
	}

	/***********************************************************************************
	 * 
	 * INDEXES
	 * 
	 ***********************************************************************************/

	@Override
	public  Index createIndex(String indexName, IndexMapper mapFunc)
	{
		GaeIndexImpl index = new GaeIndexImpl<>(mapFunc, indexName);
		indexes.add(index);
		return index;
	}

	/**
	 * GAE implementation of an index - simply exposes the Datastore property filters
	 * 
	 * @author Zach Melamed
	 */
	private class GaeIndexImpl extends Index
	{
		public GaeIndexImpl(IndexMapper mapFunc, String name)
		{
			super(mapFunc, name);
		}

		@Override
		public Iterator> iteratorOf(F f)
		{
			return entryIteratorOfQuery(newIndexQuery(f));
		}

		@Override
		public Iterator keyIteratorOf(F f)
		{
			return keyIteratorOfQuery(newIndexQuery(f).setKeysOnly());
		}

		@Override
		public Iterator valueIteratorOf(F f)
		{
			return valueIteratorOfQuery(newIndexQuery(f));
		}

		private Query newIndexQuery(F f)
		{
			Filter filter = new FilterPredicate(propertyName(), FilterOperator.EQUAL, f);
			return newQuery().setFilter(filter);
		}

		private String propertyName()
		{
			return COL_NAME_INDEX_PREFIX + name;
		}

		private F getIndexedFieldOf(V value)
		{
			return mapper.getIndexedFieldOf(value);
		}
	}

	/***********************************************************************************
	 * 
	 * DATASTORE UTILS
	 * 
	 ***********************************************************************************/

	private static final String COL_NAME_ENTRY_VALUE = "value";
	private static final String COL_NAME_INDEX_PREFIX = "_i_";
	private static final String BOGUS_ANCESTOR_KEY_NAME = " ";

	private V entityToValue(Entity entity)
	{
		Blob blob = (Blob) entity.getProperty(COL_NAME_ENTRY_VALUE);
		return serializer.bytesToObj(blob.getBytes(), valueClass);
	}

	private Entity entryToEntity(String key, V value)
	{
		Entity entity = new Entity(kind, key, ancestor);
		entity.setProperty(COL_NAME_ENTRY_VALUE, new Blob(serializer.objToBytes(value)));
		for (GaeIndexImpl index : indexes)
		{
			Object field = (value == null) ? null : index.getIndexedFieldOf(value);
			entity.setProperty(index.propertyName(), field);
		}
		return entity;
	}

	private void save(String key, V value)
	{
		ds.put(entryToEntity(key, value));
	}

	private Query newQuery()
	{
		return new Query(kind).setAncestor(ancestor);
	}

	private Iterator> entryIteratorOfQuery(Query q)
	{
		final Iterator iter = ds.prepare(q).asIterator();
		return new Iterator>()
		{
			@Override
			public boolean hasNext()
			{
				return iter.hasNext();
			}

			@Override
			public KeyValue next()
			{
				final Entity entity = iter.next();
				return new KeyValue()
				{
					@Override
					public String getKey()
					{
						return entity.getKey().getName();
					}

					@Override
					public V getValue()
					{
						return entityToValue(entity);
					}
				};
			}

			@Override
			public void remove()
			{
				throw new UnsupportedOperationException();
			}
		};
	}

	private Iterator keyIteratorOfQuery(Query q)
	{
		final Iterator iter = ds.prepare(q).asIterator();
		return new Iterator()
		{
			@Override
			public boolean hasNext()
			{
				return iter.hasNext();
			}

			@Override
			public String next()
			{
				return iter.next().getKey().getName();
			}

			@Override
			public void remove()
			{
				throw new UnsupportedOperationException();
			}
		};
	}

	private Iterator valueIteratorOfQuery(Query q)
	{
		final Iterator iter = ds.prepare(q).asIterator();
		return new Iterator()
		{
			@Override
			public boolean hasNext()
			{
				return iter.hasNext();
			}

			@Override
			public V next()
			{
				return entityToValue(iter.next());
			}

			@Override
			public void remove()
			{
				throw new UnsupportedOperationException();
			}
		};
	}

	/***********************************************************************************
	 * 
	 * SERIALIZATION
	 * 
	 ***********************************************************************************/

	public static interface Serializer
	{
		V bytesToObj(byte[] bytes, Class clz); // NOTE: if bytes is null, return null

		byte[] objToBytes(V obj); // NOTE: if obj is null, return null
	}

	private static final class JavaSerializer implements Serializer
	{
		@Override
		public V bytesToObj(byte[] bytes, Class clz)
		{
			return SerializeUtil.bytesToObj(bytes, clz);
		}

		@Override
		public byte[] objToBytes(V obj)
		{
			return SerializeUtil.objToBytes(obj);
		}
	}

	public static class KryoSerializer implements Serializer
	{
		@Override
		public V bytesToObj(byte[] bytes, Class clz)
		{
			return KryoUtil.bytesToObj(bytes, clz);
		}

		@Override
		public byte[] objToBytes(V obj)
		{
			return KryoUtil.objToBytes(obj);
		}
	}

	/***********************************************************************************
	 * 
	 * CACHE IMPLEMENTATION
	 * 
	 ***********************************************************************************/

	private class JavaSerializeCache implements Cache
	{
		private MemcacheService mc = MemcacheServiceFactory.getMemcacheService(kind);

		@Override
		@SuppressWarnings("unchecked")
		public V get(String key)
		{
			return (V) mc.get(key);
		}

		@Override
		@SuppressWarnings("unchecked")
		public Map get(Collection keys)
		{
			return (Map) (Map) mc.getAll(keys);
		}

		@Override
		public void put(String key, V value)
		{
			mc.put(key, value);
		}

		@Override
		public void put(Map values)
		{
			mc.putAll(values);
		}

		@Override
		public void delete(String key)
		{
			mc.delete(key);
		}

		@Override
		public void deleteAll()
		{
			mc.clearAll();
		}
	};

	private class CustomSerializeCache implements Cache
	{
		private MemcacheService mc = MemcacheServiceFactory.getMemcacheService(kind);

		@Override
		public V get(String key)
		{
			byte[] bytes = (byte[]) mc.get(key);
			return serializer.bytesToObj(bytes, valueClass);
		}

		@Override
		@SuppressWarnings("unchecked")
		public Map get(Collection keys)
		{
			Map values = mc.getAll(keys);
			Iterator> iter = values.entrySet().iterator();
			while (iter.hasNext())
			{
				Entry entry = iter.next();
				byte[] bytes = (byte[]) entry.getValue();
				entry.setValue(serializer.bytesToObj(bytes, valueClass));
			}
			return (Map) (Map) values;
		}

		@Override
		public void put(String key, V value)
		{
			mc.put(key, serializer.objToBytes(value));
		}

		@Override
		@SuppressWarnings("unchecked")
		public void put(Map values)
		{
			// NOTE: we make a huge assumption here, that we can modify the passed map. by doing so we rely on "inside information" that
			// this is harmless given how 'iteratorFor' is implemented
			Iterator> iter = ((Map) (Map) values).entrySet().iterator();
			while (iter.hasNext())
			{
				Entry entry = iter.next();
				Object value = entry.getValue();
				entry.setValue(serializer.objToBytes((V) value));
			}
			mc.putAll(values);
		}

		@Override
		public void delete(String key)
		{
			mc.delete(key);
		}

		@Override
		public void deleteAll()
		{
			mc.clearAll();
		}
	};
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy