package ninja.ugly.prevail.datamodel;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import ninja.ugly.prevail.chunk.Chunk;
import ninja.ugly.prevail.chunk.QueryResult;
import ninja.ugly.prevail.event.factory.DeleteEventFactory;
import ninja.ugly.prevail.event.factory.InsertEventFactory;
import ninja.ugly.prevail.event.factory.QueryEventFactory;
import ninja.ugly.prevail.event.factory.UpdateEventFactory;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* An event driven data model giving access to registered 'chunks' of data.
* <p>
* A DataModel is a container of Chunks, optionally registered to different 'segments' of the DataModel.
* Each Chunk defines its own CRUD operations on its own classes. Some Chunks might merely hold objects in
* memory, whilst others might persist objects via a network API or local database. In all cases, the
* Chunks present a consistent interface. Chunks themselves are accessed synchronously, however when
* registered on a DataModel they can be accessed asynchronously. Chunks are registered on the DataModel
* with an optional 'segment', that is a key that addresses possibly many Chunks.
*/
public class DataModel implements Closeable {
private static final String NO_SEGMENT = "NO SEGMENT";
private static final ExecutorService DEFAULT_CHUNK_EXECUTOR = Executors.newSingleThreadExecutor();
private final Map<String, List<ChunkAndExecutor>> mChunks = new HashMap<>();
/**
* Add a Chunk to the default segment of this DataModel.
* @param chunk The Chunk to add
*/
public void addChunk(final Chunk chunk) {
addChunk(NO_SEGMENT, chunk);
}
/**
* Add a Chunk to the given segment of this DataModel.
* @param segment A String naming the segment
* @param chunk The Chunk to add
*/
public void addChunk(String segment, final Chunk chunk) {
addChunkWithNullChecks(segment, chunk, DEFAULT_CHUNK_EXECUTOR);
}
/**
* Add a Chunk to the default segment of this DataModel. Run its
* operations on the given ExecutorService.
*
* @param chunk The Chunk to add
* @param executor The ExecutorService on which to run the Chunk operations. If null, a default single-threaded
* ExecutorService will be used.
*/
public void addChunk(final Chunk chunk, final ExecutorService executor) {
addChunkWithNullChecks(NO_SEGMENT, chunk, executor);
}
/**
* Add a Chunk to the given segment of this DataModel. Run its
* operations on the given ExecutorService.
*
* @param segment A String naming the segment
* @param chunk The Chunk to add
* @param executor The ExecutorService on which to run the Chunk operations. If null, a default single-threaded
* ExecutorService will be used.
*/
public void addChunk(final String segment, final Chunk chunk, final ExecutorService executor) {
addChunkWithNullChecks(segment, chunk, executor == null ? DEFAULT_CHUNK_EXECUTOR : executor);
}
/**
* Delete the given key from all Chunks registered at the default segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunk's delete method. These
* events will be dispatched, in addition to events from any DeleteEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunks' delete operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param key The key to delete.
* @param deleteEventFactories An optional list of DeleteEventFactory used to generate events for this operation.
* @param <K> The type of the key on the Chunks.
* @return A Future containing a list of results of each Chunk delete operation.
*/
public <K> Future<List<Integer>> delete(final K key, final DeleteEventFactory... deleteEventFactories) {
return delete(NO_SEGMENT, key, deleteEventFactories);
}
/**
* Delete the given key from all Chunks registered at the default segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunk's delete method. These
* events will be dispatched, in addition to events from any DeleteEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunks' delete operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param segment A String naming the segment to apply the operation. The delete operation will be propagated
* to all Chunks registered at the segment.
* @param key The key to delete.
* @param deleteEventFactories An optional list of DeleteEventFactory used to generate events for this operation.
* @param <K> The type of the key on the Chunks.
* @return A Future containing a list of results of each Chunk delete operation.
*/
public <K> Future<List<Integer>> delete(final String segment, final K key, final DeleteEventFactory... deleteEventFactories) {
List<ChunkAndExecutor> chunks = Optional.fromNullable(mChunks.get(segment)).or(Lists.<ChunkAndExecutor>newArrayListWithCapacity(0));
List<ListenableFuture<Integer>> futures = Lists.transform(chunks, new Function<ChunkAndExecutor, ListenableFuture<Integer>>() {
@Override
public ListenableFuture<Integer> apply(final ChunkAndExecutor input) {
return MoreExecutors.listeningDecorator(input.getExecutor()).submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return input.getChunk().delete(key, deleteEventFactories);
}
});
}
});
return Futures.successfulAsList(futures);
}
/**
* Insert the given value to all Chunks registered at the default segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunks' insert method. These
* events will be dispatched, in addition to events from any InsertEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunk's insert operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param value The value to insert.
* @param insertEventFactories An optional list of InsertEventFactory used to generate events for this operation.
* @param <V> The type of the value on the Chunks.
* @return A Future containing a list of results of each Chunk insert operation.
*/
public <V> Future<List<Object>> insert(final V value, final InsertEventFactory... insertEventFactories) {
return insert(NO_SEGMENT, value, insertEventFactories);
}
/**
* Insert the given value to all Chunks registered at the given segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunks' insert method. These
* events will be dispatched, in addition to events from any InsertEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunk's insert operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param segment A String naming the segment to apply the operation. The insert operation will be propagated
* to all Chunks registered at the segment.
* @param value The value to insert.
* @param insertEventFactories An optional list of InsertEventFactory used to generate events for this operation.
* @param <V> The type of the value on the Chunks.
* @return A Future containing a list of results of each Chunk insert operation.
*/
public <V> Future<List<Object>> insert(final String segment, final V value, final InsertEventFactory... insertEventFactories) {
List<ChunkAndExecutor> chunks = Optional.fromNullable(mChunks.get(segment)).or(Lists.<ChunkAndExecutor>newArrayListWithCapacity(0));
List<ListenableFuture<Object>> futures = Lists.transform(chunks, new Function<ChunkAndExecutor, ListenableFuture<Object>>() {
@Override
public ListenableFuture<Object> apply(final ChunkAndExecutor input) {
return MoreExecutors.listeningDecorator(input.getExecutor()).submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return input.getChunk().insert(value, insertEventFactories);
}
});
}
});
return Futures.successfulAsList(futures);
}
/**
* Query the given key at all Chunks registered at the given segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunks' query method. These
* events will be dispatched, in addition to events from any QueryEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunk's query operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param key The key to query.
* @param queryEventFactories An optional list of QueryEventFactory used to generate events for this operation.
* @param <K> The type of the key on the Chunks.
* @return A Future containing a list of results of each Chunk query operation.
*/
public <K> Future<List<QueryResult<Object>>> query(final K key, final QueryEventFactory... queryEventFactories) {
return query(NO_SEGMENT, key, queryEventFactories);
}
/**
* Query the given key at all Chunks registered at the default segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunks' query method. These
* events will be dispatched, in addition to events from any QueryEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunk's query operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param segment A String naming the segment to apply the operation. The query operation will be propagated
* to all Chunks registered at the segment.
* @param key The key to query.
* @param queryEventFactories An optional list of QueryEventFactory used to generate events for this operation.
* @param <K> The type of the key on the Chunks.
* @return A Future containing a list of results of each Chunk query operation.
*/
public <K> Future<List<QueryResult<Object>>> query(final String segment, final K key, final QueryEventFactory... queryEventFactories) {
List<ChunkAndExecutor> chunks = Optional.fromNullable(mChunks.get(segment)).or(Lists.<ChunkAndExecutor>newArrayListWithCapacity(0));
List<ListenableFuture<QueryResult<Object>>> futures = Lists.transform(chunks, new Function<ChunkAndExecutor, ListenableFuture<QueryResult<Object>>>() {
@Override
public ListenableFuture<QueryResult<Object>> apply(final ChunkAndExecutor input) {
return MoreExecutors.listeningDecorator(input.getExecutor()).submit(new Callable<QueryResult<Object>>() {
@Override
public QueryResult<Object> call() throws Exception {
return input.getChunk().query(key, queryEventFactories);
}
});
}
});
return Futures.successfulAsList(futures);
}
/**
* Update the given key with the given value at all Chunks registered at the default segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunks' query method. These
* events will be dispatched, in addition to events from any QueryEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunk's query operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param key The key to update.
* @param value The value to update.
* @param updateEventFactories An optional list of UpdateEventFactory used to generate events for this operation.
* @param <K> The type of the key on the Chunks.
* @param <V> The type of the value on the Chunks.
* @return A Future containing a list of results of each Chunk update operation.
*/
public <K,V> Future<List<Integer>> update(final K key, final V value, final UpdateEventFactory... updateEventFactories) {
return update(NO_SEGMENT, key, value, updateEventFactories);
}
/**
* Update the given key with the given value at all Chunks registered at the given segment of the DataModel.
* <p>
* Optional event factories may be given, which are forwarded to the Chunks' query method. These
* events will be dispatched, in addition to events from any QueryEventFactory already added
* to the respective Chunk.
* <p>
* This method returns a Future containing a List of results from each registered Chunk's query operation.
* Rather than wait on the result of this future, client code should usually await a event from an EventFactory.
*
* @param segment A String naming the segment to apply the operation. The update operation will be propagated
* to all Chunks registered at the segment.
* @param key The key to update.
* @param value The value to update.
* @param updateEventFactories An optional list of UpdateEventFactory used to generate events for this operation.
* @param <K> The type of the key on the Chunks.
* @param <V> The type of the value on the Chunks.
* @return A Future containing a list of results of each Chunk update operation.
*/
public <K, V> Future<List<Integer>> update(final String segment, final K key, final V value, final UpdateEventFactory... updateEventFactories) {
List<ChunkAndExecutor> chunks = Optional.fromNullable(mChunks.get(segment)).or(Lists.<ChunkAndExecutor>newArrayListWithCapacity(0));
List<ListenableFuture<Integer>> futures = Lists.transform(chunks, new Function<ChunkAndExecutor, ListenableFuture<Integer>>() {
@Override
public ListenableFuture<Integer> apply(final ChunkAndExecutor input) {
return MoreExecutors.listeningDecorator(input.getExecutor()).submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return input.getChunk().update(key, value, updateEventFactories);
}
});
}
});
return Futures.successfulAsList(futures);
}
private void addChunkWithNullChecks(final String segment, final Chunk chunk, final ExecutorService executor) {
ChunkAndExecutor ce = new ChunkAndExecutor(checkNotNull(chunk), checkNotNull(executor));
putIfAbsent(checkNotNull(segment), Lists.<ChunkAndExecutor>newArrayList()).add(ce);
}
private List<ChunkAndExecutor> putIfAbsent(final String segment, final List<ChunkAndExecutor> defaultValue) {
final List<ChunkAndExecutor> l;
synchronized (mChunks) {
if (mChunks.containsKey(segment)) {
l = mChunks.get(segment);
} else {
mChunks.put(segment, defaultValue);
l = defaultValue;
}
}
return l;
}
/** Close all registered chunks */
@Override
public void close() throws CompositeIOException {
synchronized (mChunks) {
List<IOException> exceptions = Lists.newArrayList();
for (ChunkAndExecutor chunkAndExecutor : Iterables.concat(mChunks.values())) {
try {
chunkAndExecutor.getChunk().close();
} catch (IOException e) {
exceptions.add(e);
}
}
if (!exceptions.isEmpty()) {
throw new CompositeIOException(exceptions);
}
}
}
public class CompositeIOException extends IOException {
private final List<IOException> mExceptions;
public CompositeIOException(List<IOException> exceptions) {
mExceptions = exceptions;
}
public List<IOException> getExceptions() {
return mExceptions;
}
}
private static final class ChunkAndExecutor {
private final Chunk mChunk;
private final ExecutorService mExecutor;
private ChunkAndExecutor(final Chunk chunk, final ExecutorService executor) {
mChunk = chunk;
mExecutor = executor;
}
public Chunk getChunk() {
return mChunk;
}
public ExecutorService getExecutor() {
return mExecutor;
}
}
}