/*
* Ext GWT 2.2.0 - Ext for GWT
* Copyright(c) 2007-2010, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.store;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.extjs.gxt.ui.client.data.ChangeEvent;
import com.extjs.gxt.ui.client.data.ChangeEventSource;
import com.extjs.gxt.ui.client.data.ChangeListener;
import com.extjs.gxt.ui.client.data.DefaultModelComparer;
import com.extjs.gxt.ui.client.data.ModelComparer;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.ModelKeyProvider;
import com.extjs.gxt.ui.client.data.SortInfo;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.BaseObservable;
import com.extjs.gxt.ui.client.event.EventType;
import com.extjs.gxt.ui.client.store.Record.RecordUpdate;
import com.extjs.gxt.ui.client.widget.ListView;
import com.extjs.gxt.ui.client.widget.form.ComboBox;
/**
* The store class encapsulates a client side cache of {@link ModelData} objects
* which provide input data for components such as the {@link ComboBox} and
* {@link ListView ListView}.
*
* <dl>
* <dt><b>Events:</b></dt>
*
* <dd><b>Store.Filter</b> : StoreEvent(store)<br>
* <div>Fires when filters are applied and removed from the store.</div>
* <ul>
* <li>store : this</li>
* </ul>
* </dd>
*
* <dd><b>Store.Update</b> : StoreEvent(store, model, record)<br>
* <div>Fires when a model has been updated via its record.</div>
* <ul>
* <li>store : this</li>
* <li>model : the model that was updated</li>
* <li>record : the record that was updated</li>
* <li>operation : the update operation being performed.</li>
* </ul>
* </dd>
*
* <dd><b>Store.BeforeClear</b> : StoreEvent(store)<br>
* <div>Fires before the store is cleared. Listeners can cancel the action by
* calling {@link BaseEvent#setCancelled(boolean)}. </div>
* <ul>
* <li>store : this</li>
* </ul>
* </dd>
*
* <dd><b>Store.Clear</b> : StoreEvent(store)<br>
* <div>Fires when the data cache has been cleared.</div>
* <ul>
* <li>store : this</li>
* </ul>
* </dd>
*
* </dl>
*
* @param <M> the model data type
*/
public abstract class Store<M extends ModelData> extends BaseObservable {
/**
* BeforeAdd event type.
*/
public static final EventType BeforeAdd = new EventType();
/**
* BeforeClear event type.
*/
public static final EventType BeforeClear = new EventType();
/**
* BeforeDataChanged event type.
*/
public static final EventType BeforeDataChanged = new EventType();
/**
* BeforeRemove event type.
*/
public static final EventType BeforeRemove = new EventType();
/**
* BeforeSort event type.
*/
public static final EventType BeforeSort = new EventType();
/**
* DataChanged event type.
*/
public static final EventType DataChanged = new EventType();
/**
* Filter event type.
*/
public static final EventType Filter = new EventType();
/**
* Filter event type.
*/
public static final EventType Sort = new EventType();
/**
* Add event type.
*/
public static final EventType Add = new EventType();
/**
* Remove event type.
*/
public static final EventType Remove = new EventType();
/**
* Update event type.
*/
public static final EventType Update = new EventType();
/**
* Clear event type.
*/
public static final EventType Clear = new EventType();
protected List<M> all = new ArrayList<M>();
protected Map<M, Record> recordMap = new HashMap<M, Record>();
protected List<M> filtered;
protected List<Record> modified = new ArrayList<Record>();
protected SortInfo sortInfo = new SortInfo();
protected StoreSorter<M> storeSorter;
protected String filterProperty;
protected String filterBeginsWith;
protected boolean filtersEnabled;
protected List<M> snapshot;
protected List<StoreFilter<M>> filters;
private ModelComparer<M> comparer;
private ChangeListener changeListener;
private boolean monitorChanges;
private ModelKeyProvider<M> keyProvider;
@SuppressWarnings("unchecked")
public Store() {
comparer = DefaultModelComparer.DEFAULT;
}
/**
* Adds a filter to the store.
*
* @param filter the store filter to add
*/
public void addFilter(StoreFilter<M> filter) {
if (filters == null) {
filters = new ArrayList<StoreFilter<M>>();
}
if (!filters.contains(filter)) {
filters.add(filter);
}
}
/**
* Adds a store listener.
*
* @param listener the listener to add
*/
public void addStoreListener(StoreListener<M> listener) {
addListener(Filter, listener);
addListener(Sort, listener);
addListener(BeforeDataChanged, listener);
addListener(DataChanged, listener);
addListener(Add, listener);
addListener(Remove, listener);
addListener(Update, listener);
addListener(Clear, listener);
}
/**
* Applies the current filters to the store.
*
* @param property the optional active property
*/
public void applyFilters(String property) {
filterProperty = property;
if (!filtersEnabled) {
snapshot = all;
}
filtersEnabled = true;
filtered = new ArrayList<M>();
for (M items : snapshot) {
if (!isFiltered(items, property)) {
filtered.add(items);
}
}
all = filtered;
if (storeSorter != null) {
applySort(false);
}
fireEvent(Filter, createStoreEvent());
}
/**
* Revert to a view of this store with no filtering applied.
*/
public void clearFilters() {
if (isFiltered()) {
filtersEnabled = false;
all = snapshot;
snapshot = null;
fireEvent(Filter, createStoreEvent());
}
}
/**
* Commit all items with outstanding changes. To handle updates for changes,
* subscribe to the Store's <i>Update</i> event, and perform updating when the
* operation parameter is {@link RecordUpdate#COMMIT}.
*/
public void commitChanges() {
List<Record> mod = new ArrayList<Record>(modified);
for (Record r : mod) {
r.commit(false);
}
modified = new ArrayList<Record>();
}
/**
* Returns true if the item is in this store.
*
* @param item the item
* @return true if container
*/
public boolean contains(M item) {
return findModel(item) != null;
}
/**
* Returns true if the two models are equal as defined by the model comparer.
*
* @param model1 the first model
* @param model2 the second model
* @return true if equals
*/
public boolean equals(M model1, M model2) {
return comparer.equals(model1, model2);
}
/**
* Filters the store using the given property.
*
* @param property the property to filter by
*/
public void filter(String property) {
filter(property, null);
}
/**
* Filters the store using the given property.
*
* @param property the property to filter by
* @param beginsWith a string the value should begin with
*/
public void filter(String property, String beginsWith) {
filterProperty = property;
filterBeginsWith = beginsWith;
applyFilters(property);
}
/**
* Returns the matching model in the cache using the model comparer to test
* for equality.
*
* @param model the model
* @return the matching model or null if no match
*/
public M findModel(M model) {
for (M m : all) {
if (comparer.equals(m, model)) {
return m;
}
}
return null;
}
public M findModel(String key) {
if (keyProvider != null) {
for (int i = 0, len = all.size(); i < len; i++) {
String id = keyProvider.getKey(all.get(i));
if (key.equals(id)) {
return all.get(i);
}
}
}
return null;
}
/**
* Returns the first model whose property matches the given value.
*
* @param property the property name
* @param value the value to match
* @return the model or null if no match
*/
public M findModel(String property, Object value) {
for (M m : all) {
Object val = m.get(property);
if (val == value || (val != null && val.equals(value))) {
return m;
}
}
return null;
}
/**
* Returns a list of all matching models whose property matches the given
* value.
*
* @param property the property name
* @param value the value to match
* @return the list of matching models
*/
public List<M> findModels(String property, Object value) {
List<M> models = new ArrayList<M>();
for (M m : all) {
Object val = m.get(property);
if (val == value || (val != null && val.equals(value))) {
models.add(m);
}
}
return models;
}
/**
* Returns the store's filters.
*
* @return the filters
*/
public List<StoreFilter<M>> getFilters() {
return filters;
}
/**
* Returns the model key provider.
*
* @return the key provider
*/
public ModelKeyProvider<M> getKeyProvider() {
return keyProvider;
}
/**
* Returns the comparer used to compare model instances.
*
* @return the comparer
*/
public ModelComparer<M> getModelComparer() {
return comparer;
}
/**
* Returns the store's models.
*
* @return the items
*/
public List<M> getModels() {
return new ArrayList<M>(all);
}
/**
* Gets all records modified since the last commit. Modified records are not
* persisted across load operations (e.g., during paging).
*
* @return a list of modified records
*/
public List<Record> getModifiedRecords() {
return new ArrayList<Record>(modified);
}
/**
* Returns the record instance for the item. Records are created on-demand and
* are cleared after a stores modifications are accepted or rejected.
*
* @param model the item
* @return the record for the item
*/
public Record getRecord(M model) {
assert model != null : "Model my not be null";
Record record = recordMap.get(model);
if (record == null) {
record = new Record(model);
record.join(this);
recordMap.put(model, record);
}
return record;
}
/**
* Returns the store sorter.
*
* @return the store sorter
*/
public StoreSorter<M> getStoreSorter() {
return storeSorter;
}
/**
* Returns true if a record exists for the given model.
*
* @param model the model
* @return true if a record exists
*/
public boolean hasRecord(M model) {
return recordMap.containsKey(model);
}
/**
* Returns true if this store is currently filtered.
*
* @return true if the store is filtered
*/
public boolean isFiltered() {
return filtersEnabled;
}
/**
* Returns true if the store is monitoring changes.
*
* @return the monitor changes state
*/
public boolean isMonitorChanges() {
return monitorChanges;
}
/**
* Cancel outstanding changes on all changed records.
*/
public void rejectChanges() {
for (Record r : new ArrayList<Record>(modified)) {
r.reject(false);
}
modified.clear();
}
/**
* Remove all items from the store and fires the <i>Clear</i> event.
*/
public void removeAll() {
StoreEvent<M> event = createStoreEvent();
if (fireEvent(BeforeClear, event)) {
for (M m : all) {
unregisterModel(m);
}
all.clear();
modified.clear();
recordMap.clear();
if (snapshot != null) {
snapshot.clear();
}
fireEvent(Clear, event);
}
}
/**
* Removes a previously added filter.
*
* @param filter the filter to remove
*/
public void removeFilter(StoreFilter<M> filter) {
if (filters != null) {
filters.remove(filter);
}
if (filters != null && filters.size() == 0 && filtersEnabled) {
clearFilters();
} else if (filtersEnabled) {
applyFilters(filterProperty);
}
}
/**
* Removes a store listener.
*
* @param listener the store listener to remove
*/
public void removeStoreListener(StoreListener<M> listener) {
removeListener(Sort, listener);
removeListener(Filter, listener);
removeListener(BeforeDataChanged, listener);
removeListener(DataChanged, listener);
removeListener(Add, listener);
removeListener(Remove, listener);
removeListener(Update, listener);
removeListener(Clear, listener);
}
/**
* Sets the model key provider which is used to uniquely identify a model from
* an id. The store itself, does not use the key provider.
*
* @param keyProvider the model key provider
*/
public void setKeyProvider(ModelKeyProvider<M> keyProvider) {
this.keyProvider = keyProvider;
}
/**
* Sets the comparer to be used when comparing model instances.
*
* @param comparer the comparer
*/
public void setModelComparer(ModelComparer<M> comparer) {
this.comparer = comparer;
}
/**
* Sets whether the store should listen to change events on its children
* (defaults to false). This method should be called prior to any models being
* added to the store when monitoring changes. Only model instances which
* implement {@link ChangeEventSource} may be monitored.
*
* @param monitorChanges true to monitor changes
*/
public void setMonitorChanges(boolean monitorChanges) {
if (changeListener == null) {
changeListener = new ChangeListener() {
public void modelChanged(ChangeEvent event) {
onModelChange(event);
}
};
}
this.monitorChanges = monitorChanges;
}
/**
* Sets the store's sorter.
*
* @param storeSorter the sorter
*/
public void setStoreSorter(StoreSorter<M> storeSorter) {
this.storeSorter = storeSorter;
}
/**
* Notifies the store that the model has been updated and fires the
* <i>Update</i> event.
*
* @param model the updated model
*/
public void update(M model) {
M m = findModel(model);
if (m != null) {
if (m != model) {
swapModelInstance(m, model);
}
StoreEvent<M> evt = createStoreEvent();
evt.setModel(model);
fireEvent(Update, evt);
}
}
protected void afterCommit(Record record) {
modified.remove(record);
fireStoreEvent(Update, RecordUpdate.COMMIT, record);
}
protected void afterEdit(Record record) {
if (record.isDirty()) {
if (!modified.contains(record)) {
modified.add(record);
}
} else {
modified.remove(record);
}
fireStoreEvent(Update, RecordUpdate.EDIT, record);
}
protected void afterReject(Record record) {
modified.remove(record);
fireStoreEvent(Update, RecordUpdate.REJECT, record);
}
protected void applySort(boolean supressEvent) {
}
protected StoreEvent<M> createStoreEvent() {
return new StoreEvent<M>(this);
}
@SuppressWarnings("unchecked")
protected void fireStoreEvent(EventType type, RecordUpdate operation, Record record) {
StoreEvent<M> evt = createStoreEvent();
evt.setOperation(operation);
evt.setRecord(record);
evt.setModel((M) record.getModel());
fireEvent(type, evt);
}
@SuppressWarnings({"unchecked", "rawtypes"})
protected boolean isFiltered(ModelData record, String property) {
if (filterBeginsWith != null && property != null) {
Object o = record.get(property);
if (o != null) {
if (!o.toString().toLowerCase().startsWith(filterBeginsWith.toLowerCase())) {
return true;
}
}
}
if (filters != null) {
for (StoreFilter filter : filters) {
boolean result = filter.select(this, record, record, property);
if (!result) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unchecked")
protected void onModelChange(ChangeEvent ce) {
if (ce.getType() == ChangeEventSource.Update) {
// ignore updates when in edit mode
M m = (M) ce.getSource();
boolean rec = hasRecord(m);
if (!rec || (rec && !getRecord(m).isEditing())) {
update((M) ce.getSource());
}
}
}
/**
* Subclasses must register any model instance being inserted into the store.
*
* @param model the model
*/
protected void registerModel(M model) {
if (monitorChanges && model instanceof ChangeEventSource) {
((ChangeEventSource) model).addChangeListener(changeListener);
}
}
protected void swapModelInstance(M oldModel, M newModel) {
M oldM = findModel(oldModel);
int index = all.indexOf(oldM);
if (index != -1) {
all.remove(oldM);
all.add(index, newModel);
unregisterModel(oldM);
registerModel(newModel);
}
if (isFiltered()) {
index = snapshot.indexOf(oldM);
if (index != -1) {
snapshot.remove(oldM);
snapshot.add(index, newModel);
}
}
}
/**
* Subclasses must unregister any model instance being removed from the store.
*
* @param model the model
*/
protected void unregisterModel(M model) {
if (monitorChanges && model instanceof ChangeEventSource) {
((ChangeEventSource) model).removeChangeListener(changeListener);
}
recordMap.remove(model);
}
}