com.tectonica.collections.KeyValueStore Maven / Gradle / Ivy
Show all versions of tectonica-commons Show documentation
package com.tectonica.collections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import com.tectonica.collections.KeyValueStore.KeyValue;
/**
* Simple, yet powerful, framework (and approach) for handling a key-value store. It allows for multiple concurrent readers but a single
* concurrent writer of each entry. It also provides a read-before-update mechanism which makes life simpler and keeps data consistent.
* Indexing is also supported as part of the framework. The interface is intuitive and straightforward. This class itself is abstract and
* subclasses are included for several backend persistence engines, including in-memory. (which is great for development).
*
* The usage of this framework would probably yield the most benefit when used to prototype a data model, where changes are frequent and not
* backwards-compatible. However, in more than a few scenarios, this framework can be used in production. The heavy-lifting is done by the
* backend database either way.
*
* @author Zach Melamed
*/
public abstract class KeyValueStore implements Iterable>
{
public interface KeyValue
{
K getKey();
V getValue();
}
public interface KeyMapper
{
public K getKeyOf(V value);
}
protected final KeyMapper keyMapper;
/**
* creates a new key-value store manager
*
* @param keyMapper
* this optional parameter is suitable in situations where the key of an entry can be inferred from its value directly
* (as opposed to when the key and value are stored separately). when provided, several convenience methods become applicable
*/
protected KeyValueStore(KeyMapper keyMapper)
{
this.keyMapper = keyMapper;
}
/***********************************************************************************
*
* GETTERS
*
* many of the non-abstract methods here offer somewhat of a naive implementation.
* subclasses are welcome to override with their own efficient implementation.
*
***********************************************************************************/
public abstract V get(K key);
public abstract Iterator> iterator();
public Iterator keyIterator()
{
final Iterator> iter = iterator();
return new Iterator()
{
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public K next()
{
return iter.next().getKey();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public Iterator valueIterator()
{
final Iterator> iter = iterator();
return new Iterator()
{
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public V next()
{
return iter.next().getValue();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public abstract Iterator> iteratorFor(Set keySet);
public Iterator valueIteratorFor(Set keySet)
{
final Iterator> iter = iteratorFor(keySet);
return new Iterator()
{
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public V next()
{
return iter.next().getValue();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public Set keySet()
{
return iterateInto(keyIterator(), new HashSet());
}
public List values()
{
return iterateInto(valueIterator(), new ArrayList());
}
public List valuesFor(Set keySet)
{
return iterateInto(valueIteratorFor(keySet), new ArrayList());
}
/***********************************************************************************
*
* SETTERS (UTILS)
*
***********************************************************************************/
/**
* an interface for managing a modification process of an existing entry. there are two types of such modification:
*
* - using {@link KeyValueStore#replace(Object, Object)}: in such case only the {@link #commit(Object)} method will be invoked. it
* will be passed an updated value for an existing key.
*
- using {@link KeyValueStore#update(Object, Updater)}: in such case first the {@link #getModifiableValue()} method will be invoked,
* generating an instance for the caller to safely modify, and then the {@link #commit(Object)} method will be invoked on that modified
* instance.
*
* both methods are invoked under the concurrency protection a lock provided with {@link KeyValueStore#getModificationLock(Object)}.
*/
protected interface Modifier
{
/**
* Returns an instance that can be safely modified by the caller. During this modification, calls to {@link #getValue()} will return
* the unchanged value. If the instance was indeed modified by the caller, and no exception occurred in the process, the method
* {@link #commit(Object)} will be invoked.
*
* NOTE: this method is called only on a locked entry
*/
V getModifiableValue();
/**
* Makes the changes to an entry permanent. After this method finishes, calls to {@link #getValue()} will return the updated value.
*
* NOTE: this method is called only on a locked entry
*/
void commit(V value);
}
protected static enum ModificationType
{
UPDATE, REPLACE;
}
/**
* required to return a (short-lived) instance of {@link Modifier} corresponding to a given key, or a null if the passed key can't be
* updated (because it doesn't exist, or for another reason). The returned instance is used for a one-time modification.
*/
protected abstract Modifier getModifier(K key, ModificationType purpose);
/**
* expected to return a global lock for a specific key (global means that it blocks other machines as well, not just the current
* instance). It is a feature of this framework that only one updater is allowed for an entry at each given moment, so whenever an
* updater thread starts the (non-atomic) process of updating the entry, all other attempts should be blocked.
*/
protected abstract Lock getModificationLock(K key);
public static abstract class Updater
{
private boolean stopped = false;
private boolean changed = false;
protected void stopIteration()
{
stopped = true;
}
public boolean isChanged()
{
return changed;
}
/**
* method inside which an entry may be safely modified with no concurrency or shared-state concerns
*
* @param value
* thread-safe entry on which the modifications are to be performed. never a null.
* @return
* true if the method indeed changed the entry
*/
public abstract boolean update(V value);
/**
* executes after the persistence has happened and getters return the new value. however, the entry is still locked at that point
* and can be addressed without any concern that another updater tries to start a modification.
*
* IMPORTANT: do not make any modifications to the passed entry inside this method
*/
public void postCommit(V value)
{}
/**
* called if an update process was requested on a non existing key
*/
public void entryNotFound()
{}
}
/***********************************************************************************
*
* SETTERS
*
***********************************************************************************/
/**
* inserts a new entry, whose key doesn't already exist in storage. it's a faster and more resource-efficient way to insert entries
* compared to {@link #replace(Object, Object)} as it doesn't use any locking. do not use if you're not completely sure whether the key
* already exists. the behavior of the store in such case is undetermined and implementation-dependent.
*/
public abstract void insert(K key, V value);
/**
* inserts or updates an entry. if you're sure that the entry is new (i.e. its key doesn't already exist), use the more efficient
* {@link #insert(Object, Object)} instead
*/
public void replace(K key, V value)
{
Lock lock = getModificationLock(key);
lock.lock();
try
{
Modifier modifier = getModifier(key, ModificationType.REPLACE);
if (modifier == null)
insert(key, value);
else
modifier.commit(value);
}
finally
{
lock.unlock();
}
}
public V update(K key, Updater updater)
{
Lock lock = getModificationLock(key);
lock.lock();
try
{
Modifier modifier = getModifier(key, ModificationType.UPDATE);
if (modifier == null)
{
updater.entryNotFound();
return null;
}
V value = modifier.getModifiableValue();
if (value == null)
{
updater.entryNotFound();
return null;
}
updater.changed = updater.update(value);
if (updater.changed)
modifier.commit(value);
updater.postCommit(value);
return value;
}
finally
{
lock.unlock();
}
}
public void update(Set keySet, Updater updater)
{
for (K key : keySet)
{
update(key, updater);
if (updater.stopped)
break;
}
}
/***********************************************************************************
*
* SETTERS (CONVENIENCE)
*
***********************************************************************************/
/**
* convenience method to update all entries
*/
public void updateAll(Updater updater)
{
update(keySet(), updater);
}
/**
* convenience method applicable when {@code keyMapper} is provided
*
* @see {@link #update(Object, Updater)}
*/
public V updateValue(V value, Updater updater)
{
return update(keyMapper.getKeyOf(value), updater);
}
/**
* convenience method applicable when {@code keyMapper} is provided
*
* @see {@link #insert(Object, Object)}
*/
public void insertValue(V value)
{
insert(keyMapper.getKeyOf(value), value);
}
/**
* convenience method applicable when {@code keyMapper} is provided
*
* @see {@link #replace(Object, Object)}
*/
public void replaceValue(V value)
{
replace(keyMapper.getKeyOf(value), value);
}
/***********************************************************************************
*
* DELETERS
*
***********************************************************************************/
public abstract void delete(K key);
// TODO: add delete (keySet)
public abstract void truncate();
/***********************************************************************************
*
* INDEXES
*
***********************************************************************************/
public abstract Index createIndex(String indexName, IndexMapper mapFunc);
public interface IndexMapper
{
public F getIndexedFieldOf(V entry);
}
public static abstract class Index
{
protected final IndexMapper mapper;
protected final String name;
protected Index(IndexMapper mapper, String name)
{
this.mapper = mapper;
this.name = name;
}
// ///////////////////////////////////////////////////////////////////////////////////////
public abstract Iterator keyIteratorOf(F f);
public abstract Iterator valueIteratorOf(F f);
// ///////////////////////////////////////////////////////////////////////////////////////
public Set keySetOf(F f)
{
return KeyValueStore.iterateInto(keyIteratorOf(f), new HashSet());
}
public List valuesOf(F f)
{
return KeyValueStore.iterateInto(valueIteratorOf(f), new ArrayList());
}
public Iterable asKeyIterableOf(F f)
{
return KeyValueStore.iterableOf(keyIteratorOf(f));
}
public Iterable asValueIterableOf(F f)
{
return KeyValueStore.iterableOf(valueIteratorOf(f));
}
public K getFirstKey(F f)
{
Iterator iter = keyIteratorOf(f);
if (iter.hasNext())
return iter.next();
return null;
}
public V getFirstValue(F f)
{
Iterator iter = valueIteratorOf(f);
if (iter.hasNext())
return iter.next();
return null;
}
public String getName()
{
return name;
}
}
/***********************************************************************************
*
* INTERNAL UTILS
*
***********************************************************************************/
protected static > T iterateInto(Iterator iter, T collection)
{
while (iter.hasNext())
collection.add(iter.next());
return collection;
}
protected static Iterable iterableOf(final Iterator iter)
{
return new Iterable()
{
@Override
public Iterator iterator()
{
return iter;
}
};
}
}