
com.zipwhip.binding.DefaultStore Maven / Gradle / Ivy
package com.zipwhip.binding;
import com.zipwhip.events.*;
import com.zipwhip.util.AutoNumberGenerator;
import com.zipwhip.util.Generator;
import com.zipwhip.concurrent.DefaultObservableFuture;
import com.zipwhip.concurrent.ObservableFuture;
import com.zipwhip.util.KeyValuePair;
import java.util.Comparator;
import java.util.EventObject;
import java.util.Set;
/**
* Created by IntelliJ IDEA.
* User: Michael
* Date: 11/26/11
* Time: 8:43 PM
*
* A store contains records. I suspect that only 1 implementation will only ever be needed. I wonder if i could get away
* without using an interface. My worry is that someone will want to use a different backing store than MixedCollection
* (one that is more efficient, such as a sophisticated Tree structure), so i went with an interface.
*
* This store is something GUI renderers/builders can use. They bind to the store, and it can be updated when
* the store reports changes.
*/
public class DefaultStore implements Store {
private static final Generator ID_GENERATOR = new AutoNumberGenerator();
/**
* A record has been saved/committed.
*/
protected ObservableHelper> onChange = new ObservableHelper>();
/**
* A record has been added.
*/
protected ObservableHelper> onAdd = new ObservableHelper>();
/**
* A record has been removed.
*/
protected ObservableHelper> onRemove = new ObservableHelper>();
/**
* The complete data set has been loaded or reloaded.
*/
protected ObservableHelper onLoad = new ObservableHelper();
/**
* The backing data keyed by ID.
*/
protected MixedCollection data = new MixedCollection();
// cached for efficiency
private EventObject eventObject;
private DataProxy proxy;
private DataReader reader;
// for async hits to load() data
private ObservableFuture lastLoadingFuture;
private ObservableFuture lastProxyLoadingFuture;
// this will listen for dataSource updates
private Observer> onLoadCompleteCallback = new Observer>() {
@Override
public void notify(Object dataEventObjectObservable, ObservableFuture future) {
// make sure we use this one
if (future != lastProxyLoadingFuture){
// this is not the most current one. ignore this request.
return;
}
try {
handleImmediatelyDone(future, lastLoadingFuture);
} catch (Exception e) {
e.printStackTrace();
}
}
};
public DefaultStore() {
}
/**
* For loading data into this store.
*
* @param reader the thing that creates records from raw data
* @param proxy the thing that finds raw data somewhere
*/
public void setDataSource(DataReader reader, DataProxy proxy) {
this.reader = reader;
this.proxy = proxy;
}
/**
* Load data via the proxy, and pump the records into the store
*
* @throws Exception
*/
public ObservableFuture load() throws Exception {
if (lastLoadingFuture != null) {
// cancel the old ones.
lastLoadingFuture.cancel();
lastProxyLoadingFuture.cancel();
}
// this might take a long time.
// unfortunately i can't sync during it.
// but this means that after a LOAD operation is requested,
// any adds that someone does will be overridden by the proxy.load() return value.
// note: unfortunately this is synchronous.
lastProxyLoadingFuture = proxy.load();
lastLoadingFuture = new DefaultObservableFuture(this);
// listen for completion of this future.
lastProxyLoadingFuture.addObserver(onLoadCompleteCallback);
// this whitespace might be all it takes to miss a callback, so we have to listen first, check second.
if (handleImmediatelyDone(lastProxyLoadingFuture, lastLoadingFuture)) {
lastProxyLoadingFuture.removeObserver(onLoadCompleteCallback);
}
return lastLoadingFuture;
}
/**
* This future is the future of the proxy
*
* @param internal
* @param external
* @throws Exception
* @return if it was done or not
*/
private synchronized boolean handleImmediatelyDone(ObservableFuture internal, ObservableFuture external) throws Exception {
// if it's already done, we need to do some work.
if (internal.isDone()) {
if (internal.isSuccess()){
// already done! shit! we missed it!
parseAndLoad(internal.getResult());
external.setSuccess(null);
} else if (internal.isCancelled()) {
// cascade the cancellation
external.cancel();
} else {
// must be in error?
external.setFailure(internal.getCause());
}
return true;
}
return false;
}
private Observer> onValueChanged = new Observer>() {
@Override
public void notify(Object observable, DataEventObject e) {
if (e != null && e.getData() != null) {
if (!contains(e.getData().getRecordId())) {
throw new RuntimeException("Assertion error. We've received an event for a record we do not know about. We probably didn't unregister a listener through the remove() event.");
}
}
// one of our records changed!
// the Observable here is the record.
if (onChange != null) {
@SuppressWarnings(value="unchecked") // Guaranteed type safe as R is defined as and e is a Record which is an interface
R record = (R) e.getData();
// we need to find the index for this.
int index = data.indexOfValue(record);
// rethrow but with an index.
onChange.notifyObservers(this, getEventObject(record, index));
}
}
};
private void remove(R record) {
if (record == null) {
return;
}
Long id = record.getRecordId();
if (!contains(id)) {
return;
}
// need to know where it is so we can update the GUI efficiently
int index = indexOf(id);
// kill from our data backing.
// NOTE: this MUST pierce the veil of filtering
data.remove(id);
// stop listening for changes.
record.onChange().removeObserver(this.onValueChanged);
if (this.onRemove != null) {
this.onRemove.notifyObservers(this, getEventObject(record, index));
}
}
@Override
public synchronized void remove(Long id) {
R record = get(id);
remove(record);
}
@Override
public synchronized void add(R record) {
int index = silentAdd(record);
if (this.onAdd != null) {
this.onAdd.notifyObservers(this, new OrderedDataEventObject(this, record, index));
}
}
private int silentAdd(R record) {
if (record == null) {
throw new NullPointerException("Record is null passed into add() to store");
}
Long id = record.getRecordId();
if (id == null) {
id = ID_GENERATOR.next();
try {
record.setRecordId(id);
} catch (Exception e) {
// this can't happen if we have exclusive access to the object (2 threads?)
}
}
R existing = data.get(id);
if (existing != null) {
throw new RuntimeException("The store already contains this");
}
// add it to our internal memory
int index = data.add(id, record);
// listen for change events. (null safe version)
record.onChange().addObserver(this.onValueChanged);
return index;
}
@Override
public R get(Long id) {
return data.get(id);
}
@Override
public int size() {
return data.size();
}
@Override
public Observable> onChange() {
if (this.onChange == null) {
this.onChange = new ObservableHelper>();
}
return this.onChange;
}
@Override
public Observable onLoad() {
return onLoad;
}
@Override
public Observable> onAdd() {
return this.onAdd;
}
@Override
public Observable> onRemove() {
return this.onRemove;
}
@Override
public R getAt(int index) {
return data.getAt(index);
}
@Override
public int indexOf(Long id) {
return data.indexOfKey(id);
}
@Override
public boolean contains(Long recordId) {
return data.containsKey(recordId);
}
public void sort(final Comparator comparator) {
// the 2 different data structures require different API's
// let's wrap another comparator around to adapt between them.
data.sort(new Comparator>() {
public int compare(KeyValuePair o1, KeyValuePair o2) {
return comparator.compare(o1.getValue(), o2.getValue());
}
});
if (onLoad != null) {
onLoad.notifyObservers(this, getEventObject());
}
}
@Override
public void setFilter(final Filter filter) {
// do the filter (NOTE: we ignore events from the MixedCollection)
// so we have to throw our own event manually
data.setFilter(new Filter>() {
public boolean call(KeyValuePair item) throws Exception {
return filter.call(item.getValue());
}
});
// announce that a full redraw is necessary
if (onLoad != null) {
onLoad.notifyObservers(this, new EventObject(this));
}
}
@Override
public boolean isFiltered() {
return data.isFiltered();
}
@Override
public void clearFilter() {
data.clearFilter();
// announce that a full redraw is necessary
if (onLoad != null) {
onLoad.notifyObservers(this, new EventObject(this));
}
}
private OrderedDataEventObject getEventObject(R record, int index) {
return new OrderedDataEventObject(this, record, index);
}
private OrderedDataEventObject getEventObject(R record) {
return new OrderedDataEventObject(this, record, indexOf(record.getRecordId()));
}
private void parseAndLoad(D data) throws Exception {
@SuppressWarnings(value="unchecked") // Guaranteed type safe as R is defined as
Set records = (Set) reader.read(data); // parse it into records
synchronized (this) {
// clear the data first.
this.data.clear();
// repopulate.
// also, dont let anyone else do any adds during this time
synchronized (records) {
for (R record : records) {
silentAdd(record);
}
}
}
// announce the change
if (onLoad != null) {
onLoad.notifyObservers(this, getEventObject());
}
}
public EventObject getEventObject() {
if (eventObject == null) {
synchronized (this) {
if (eventObject == null) {
eventObject = new EventObject(this);
}
}
}
return eventObject;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy