Java tutorial
/** * Sencha GXT 4.0.0 - Sencha for GWT * Copyright (c) 2006-2015, Sencha Inc. * * licensing@sencha.com * http://www.sencha.com/products/gxt/license/ * * ================================================================================ * Open Source License * ================================================================================ * This version of Sencha GXT is licensed under the terms of the Open Source GPL v3 * license. You may use this license only if you are prepared to distribute and * share the source code of your application under the GPL v3 license: * http://www.gnu.org/licenses/gpl.html * * If you are NOT prepared to distribute and share the source code of your * application under the GPL v3 license, other commercial and oem licenses * are available for an alternate download of Sencha GXT. * * Please see the Sencha GXT Licensing page at: * http://www.sencha.com/products/gxt/license/ * * For clarification or additional options, please contact: * licensing@sencha.com * ================================================================================ * * * ================================================================================ * Disclaimer * ================================================================================ * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY, * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING. * ================================================================================ */ package com.sencha.gxt.data.shared; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerRegistration; import com.sencha.gxt.core.client.ValueProvider; import com.sencha.gxt.core.shared.FastMap; import com.sencha.gxt.core.shared.event.GroupingHandlerRegistration; import com.sencha.gxt.data.shared.event.StoreAddEvent; import com.sencha.gxt.data.shared.event.StoreAddEvent.StoreAddHandler; import com.sencha.gxt.data.shared.event.StoreClearEvent; import com.sencha.gxt.data.shared.event.StoreClearEvent.StoreClearHandler; import com.sencha.gxt.data.shared.event.StoreDataChangeEvent; import com.sencha.gxt.data.shared.event.StoreDataChangeEvent.StoreDataChangeHandler; import com.sencha.gxt.data.shared.event.StoreFilterEvent; import com.sencha.gxt.data.shared.event.StoreFilterEvent.StoreFilterHandler; import com.sencha.gxt.data.shared.event.StoreHandlers; import com.sencha.gxt.data.shared.event.StoreHandlers.HasStoreHandlers; import com.sencha.gxt.data.shared.event.StoreRecordChangeEvent; import com.sencha.gxt.data.shared.event.StoreRecordChangeEvent.StoreRecordChangeHandler; import com.sencha.gxt.data.shared.event.StoreRemoveEvent; import com.sencha.gxt.data.shared.event.StoreRemoveEvent.StoreRemoveHandler; import com.sencha.gxt.data.shared.event.StoreSortEvent; import com.sencha.gxt.data.shared.event.StoreSortEvent.StoreSortHandler; import com.sencha.gxt.data.shared.event.StoreUpdateEvent; import com.sencha.gxt.data.shared.event.StoreUpdateEvent.StoreUpdateHandler; /** * Store is a client-side cache for collections of data. Modifications made to * the Store via Records are not passed right away to the data, allowing for the * changes to be committed or rolled back. * * @param <M> the model type */ public abstract class Store<M> implements HasStoreHandlers<M> { /** * Represents a change that can occur to a given model. This interface may not * be required, it will depend on if legacy cases need it or not to allow * PropertyChange to be implemented another way * * @param <M> the model type * @param <V> the value type (for the changed property in the model) */ public interface Change<M, V> { /** * Gets a tag for this change, so that two changes, both making * modifications to the same field, can replace each other, as they must be * mutually exclusive * * @return the tag */ Object getChangeTag(); /** * Gets the value that will be set on the model in modify(M). * * @return the value */ V getValue(); /** * Checks to see if the given model already has the change * * @param model the model * @return true if model already has the change */ boolean isCurrentValue(M model); /** * Make the change recorded here to the given model * * @param model the model */ void modify(M model); } /** * ValueProvider-based change impl - takes a ValueProvider and the new value * to be changed. The ValueProvider instance should be reused, as it will be * the objecttag. * * @param <M> the model type * @param <V> the value type (for the changed property in the model) */ public static class PropertyChange<M, V> implements Change<M, V> { private final ValueProvider<? super M, V> access; private final V value; /** * Creates a new property change. * * @param propertyAccess the changed property * @param value the changed value */ public PropertyChange(ValueProvider<? super M, V> propertyAccess, V value) { access = propertyAccess; this.value = value; } public final Object getChangeTag() { return access.getPath(); } public V getValue() { return value; } public boolean isCurrentValue(M model) { return value == null ? access.getValue(model) == null : value.equals(access.getValue(model)); } public final void modify(M model) { access.setValue(model, value); } } /** * Records wrap model instances and provide specialized editing features, * including modification tracking and editing capabilities. */ public class Record { private final M model; private final Map<Object, Change<M, ?>> changes = new HashMap<Object, Store.Change<M, ?>>(); /** * Creates a new record that wraps the given model. * * @param model the model to be wrapped by this record */ public Record(M model) { this.model = model; } /** * Adds a change to the data in this Record. If auto commit is enabled, the * change will be made directly to the model, else the change will be queued * up until commit() is called. * * @param <V> the value type (for the changed property in the model) * @param property the property to change * @param value the changed value */ public <V> void addChange(ValueProvider<? super M, V> property, V value) { if (!isAutoCommit) { Change<M, V> c = new PropertyChange<M, V>(property, value); if (c.isCurrentValue(model)) { changes.remove(c.getChangeTag()); if (changes.size() == 0) { modifiedRecords.remove(this); } } else { changes.put(c.getChangeTag(), c); modifiedRecords.add(this); } fireEvent(new StoreRecordChangeEvent<M>(this, property)); } else { property.setValue(model, value); fireEvent(new StoreUpdateEvent<M>(Collections.singletonList(this.model))); } } /** * Commits the changes to the model tracked by this record. */ public void commit(boolean fireEvent) { if (isDirty()) { for (Change<M, ?> c : changes.values()) { assert c.isCurrentValue( model) == false : "Current value was somehow stored in a record's change set!"; c.modify(model); } changes.clear(); if (fireEvent) { fireEvent(new StoreUpdateEvent<M>(Collections.singletonList(this.model))); } } } /** * Gets the current Change object applied to that property, if any. * * @param <V> the value type (for the changed property in the model) * @param property the changed property * @return a Change object, or null if the value is the default */ @SuppressWarnings("unchecked") public <V> Change<M, V> getChange(ValueProvider<? super M, V> property) { // This will be typesafe ONLY if only addChange(ValueProvider<M,V>, V) is // called // if we keep this, kill the other addChange, or the Change interface // itself return (Change<M, V>) changes.get(property.getPath()); } /** * Returns all changes. * * @return collection of the changes */ public Collection<Change<M, ?>> getChanges() { return changes.values(); } /** * Returns the wrapped model instance. * * @return the model */ public M getModel() { return model; } /** * Gets the current value of this property in the record, whether it is * saved or not. * * The value on the model in this record can be obtained by calling * property.getValue(record.getModel()) * * @param <V> the value type (for the property in the model) * @param property the property containing the value to get * @return current value of this property */ public <V> V getValue(ValueProvider<? super M, V> property) { Change<M, V> change = getChange(property); if (change == null) { return property.getValue(model); } else { return change.getValue(); } } /** * Returns true if the record has uncommitted changes. * * @return the dirty state */ public boolean isDirty() { return !changes.isEmpty(); } /** * Rejects a single change made to the Record since its creation, or since * the last commit operation. * * Fires a {@link StoreUpdateEvent} if a change is made. * * @param property the property of the model to revert */ public void revert(ValueProvider<? super M, ?> property) { if (changes.remove(property.getPath()) != null) { fireEvent(new StoreUpdateEvent<M>(Collections.singletonList(this.model))); } } /** * Rejects all changes made to the Record since either creation, or the last * commit operation. Modified fields are reverted to their original values. */ public void revert() { changes.clear(); fireEvent(new StoreUpdateEvent<M>(Collections.singletonList(this.model))); } } /** * Defines the interface for store filters. * * Filters receive only the last stored version of data, in contrast to 2.x, * where the current changed value was always available. To get the change * value, ask the Store for a Record instance. * * @param <M> the model type */ public interface StoreFilter<M> { /** * Indicates if the given item should be kept visible in the store. If false * is returned, the item will not be visible, and if true is returned, the * item may be visible, pending any other filter's decision. * * @param store the store containing the item to be kept visible * @param parent the parent of the item (for hierarchical stores) * @param item the item to keep visible * @return true if the item will be visible, false if not */ public boolean select(Store<M> store, M parent, M item); } /** * Sort information for a Store to use. Constructors make it possible to * easily sort based on either a property of the items in the store, or sort * the items themselves. * * Sort direction may be changed after creation, but the comparator is fixed. * A new StoreSortInfo object must be created to change the comparator. * * @param <M> the model type */ public static class StoreSortInfo<M> implements Comparator<M> { private SortDir direction; private final Comparator<? super M> comparator; private final ValueProvider<? super M, ?> valueProvider; /** * Creates a sort info object based on the given comparator to act on the * item itself. Complex comparators can easily be built in this way, instead * of adding multiple StoreSortInfo objects, or using one of the other * constructors. * * @param itemComparator the comparator to use to sort the items * @param direction the sort direction */ public StoreSortInfo(Comparator<? super M> itemComparator, SortDir direction) { this.comparator = itemComparator; this.direction = direction; this.valueProvider = null; } /** * Creates a sort info object to act on a property of the items and a custom * comparator for that property's type. * * @param <V> the property type * @param property the sort property * @param itemComparator the comparator to use in the sort * @param direction the sort direction */ public <V> StoreSortInfo(final ValueProvider<? super M, V> property, final Comparator<? super V> itemComparator, SortDir direction) { this.valueProvider = property; this.direction = direction; this.comparator = new Comparator<M>() { public int compare(M o1, M o2) { return itemComparator.compare(property.getValue(o1), property.getValue(o2)); } }; } /** * Convenience constructor for sorting based on a {@link Comparable} * property of items in the store. * * @param <V> the property type * @param property the sort property * @param direction the sort direction */ public <V extends Comparable<V>> StoreSortInfo(final ValueProvider<? super M, V> property, SortDir direction) { this.valueProvider = property; this.direction = direction; this.comparator = new Comparator<M>() { public int compare(M o1, M o2) { V v1 = property.getValue(o1); V v2 = property.getValue(o2); if ((v1 == null & v2 != null) || (v1 != null && v2 == null)) { return v1 == null ? -1 : 1; } if (v1 == null & v2 == null) { return 0; } return v1.compareTo(v2); } }; } @Override public int compare(M o1, M o2) { int val = comparator.compare(o1, o2); return direction == SortDir.ASC ? val : -val; } /** * Returns the current sort direction for this sort info. * * @return the current sort direction */ public SortDir getDirection() { return direction; } /** * If the sort info object is configured to act on a property of the items, * returns the path that the property's ValueProvider makes available, * otherwise returns empty string. * * @return the path for the property value provider or empty string if no * value provider is configured */ public String getPath() { return valueProvider != null ? valueProvider.getPath() : ""; } /** * Returns the sort property's ValueProvider. * * @return the sort property's ValueProvider or null if one has not been * configured */ public ValueProvider<? super M, ?> getValueProvider() { return valueProvider; } /** * Sets a new sort direction. Will not take effect until * {@link Store#applySort(boolean)} is called on the store containing the * sort info. * * @param direction the sort direction */ public void setDirection(SortDir direction) { this.direction = direction; } } // TODO lazily init these? private final Map<String, Record> records = new FastMap<Record>(); private Set<Record> modifiedRecords = new HashSet<Record>(); private boolean isAutoCommit = false; private ModelKeyProvider<? super M> keyProvider; private List<StoreSortInfo<M>> comparators = new ArrayList<StoreSortInfo<M>>(); private HandlerManager handlerManager; private boolean filtersEnabled; /** * Using a LinkedHashSet so each filter can only be added once, and order * matters */ private LinkedHashSet<StoreFilter<M>> filters; /** * Creates a store with the given key provider. The key provider is * responsible for returning a unique key for a given model * * @param keyProvider the key provider, responsible for returning a unique key * for a given model */ public Store(ModelKeyProvider<? super M> keyProvider) { this.keyProvider = keyProvider; } /** * Adds the filter to the end of the store's set of filters. Runs the filters * again if they are enabled. * * @param filter the filter to add */ public void addFilter(StoreFilter<M> filter) { if (filters == null) { filters = new LinkedHashSet<Store.StoreFilter<M>>(); } filters.add(filter); if (filtersEnabled) { // TODO consider not running the full set of filters, just limiting what // is already visible applyFilters(); } } /** * Adds the sort info at the specified index. The store will be sorted after * this change. * * @param index the sort index * @param info the sort info */ public void addSortInfo(int index, StoreSortInfo<M> info) { comparators.add(index, info); applySort(false); } /** * Adds the specified sort info to the store. The store will then be sorted * based on this new sort info. * * @param info the sort info */ public void addSortInfo(StoreSortInfo<M> info) { comparators.add(info); applySort(false); } @Override public HandlerRegistration addStoreAddHandler(StoreAddHandler<M> handler) { return ensureHandlers().addHandler(StoreAddEvent.getType(), handler); } @Override public HandlerRegistration addStoreClearHandler(StoreClearHandler<M> handler) { return ensureHandlers().addHandler(StoreClearEvent.getType(), handler); } @Override public HandlerRegistration addStoreDataChangeHandler(StoreDataChangeHandler<M> handler) { return ensureHandlers().addHandler(StoreDataChangeEvent.getType(), handler); } @Override public HandlerRegistration addStoreFilterHandler(StoreFilterHandler<M> handler) { return ensureHandlers().addHandler(StoreFilterEvent.getType(), handler); } @Override public HandlerRegistration addStoreHandlers(StoreHandlers<M> handlers) { GroupingHandlerRegistration reg = new GroupingHandlerRegistration(); reg.add(addStoreAddHandler(handlers)); reg.add(addStoreRemoveHandler(handlers)); reg.add(addStoreClearHandler(handlers)); reg.add(addStoreDataChangeHandler(handlers)); reg.add(addStoreFilterHandler(handlers)); reg.add(addStoreUpdateHandler(handlers)); reg.add(addStoreRecordChangeHandler(handlers)); reg.add(addStoreSortHandler(handlers)); return reg; } @Override public HandlerRegistration addStoreRecordChangeHandler(StoreRecordChangeHandler<M> handler) { return ensureHandlers().addHandler(StoreRecordChangeEvent.getType(), handler); } @Override public HandlerRegistration addStoreRemoveHandler(StoreRemoveHandler<M> handler) { return ensureHandlers().addHandler(StoreRemoveEvent.getType(), handler); } @Override public HandlerRegistration addStoreSortHandler(StoreSortHandler<M> handler) { return ensureHandlers().addHandler(StoreSortEvent.getType(), handler); } @Override public HandlerRegistration addStoreUpdateHandler(StoreUpdateHandler<M> handler) { return ensureHandlers().addHandler(StoreUpdateEvent.getType(), handler); } /** * Tells the store to re-apply sort settings and to fire an event when * complete. Must be called when manipulating the sort settings directly * instead of through the store, or the sort order will not change. * * Automatically called after {@link #addSortInfo(StoreSortInfo)}, * {@link #addSortInfo(int, StoreSortInfo)}, and {@link #clearSortInfo()}. * * @param suppressEvent true to suppress event from firing */ public abstract void applySort(boolean suppressEvent); /** * Removes all of the sort info from the store, so subsequent calls to * applySort will not change the order. */ public void clearSortInfo() { comparators.clear(); } /** * Commits the outstanding changes. */ public void commitChanges() { List<M> committedData = new ArrayList<M>(); for (Record r : modifiedRecords) { r.commit(false); committedData.add(r.getModel()); } modifiedRecords.clear(); fireEvent(new StoreUpdateEvent<M>(committedData)); } /** * Finds the matching model using the store's key provider. This can be used * to check if an item is present in the store, as it will return null if not. * * @param model target model * @return the matching model or null if the model is not present */ public M findModel(M model) { return findModelWithKey(getKeyProvider().getKey(model)); } /** * Finds the model with the given key, using {@link ModelKeyProvider} as * necessary. * * @param key the key of the model to find * @return the model with the given key, or null if the model cannot be found * in the store */ public abstract M findModelWithKey(String key); public void fireEvent(GwtEvent<?> event) { if (handlerManager != null) { handlerManager.fireEvent(event); } } /** * Returns a list of all items contained in the store. Modifying this list * will not change the store, as this is a copy of the contents of the store. * Note also that because this is a copy, this can be a expensive call to * make. * * @return the list of items */ public abstract List<M> getAll(); /** * Returns the stores filters. * * @return the filters */ public LinkedHashSet<StoreFilter<M>> getFilters() { return filters; } /** * Returns the stores model key provider. * * @return the model key provider */ public ModelKeyProvider<? super M> getKeyProvider() { return keyProvider; } /** * Returns a list of records that have been changed and not committed. * * @return the list of modified records */ public Collection<Store<M>.Record> getModifiedRecords() { return Collections.unmodifiableCollection(modifiedRecords); } /** * Gets the current Record instance for the given item. If a Record doesn't already exist, one * will be created. Use {@link #hasRecord(Object)} to check if you don't want to create a new one. * * @param data the data key * @return the record */ public Record getRecord(M data) { String key = getKeyProvider().getKey(data); Record rec = records.get(key); if (rec == null) { rec = new Record(data); records.put(key, rec); } return rec; } /** * Gets the list of sort info objects. This list may be modified directly, but * before it takes effect, {@link #applySort(boolean)} must be called. Note * that {@link #addSortInfo(StoreSortInfo)} and * {@link #addSortInfo(int, StoreSortInfo)} will add the new sort info the * this list, and then call applySort directly. * * @return the current mutable list of StoreSortInfo instances */ public List<StoreSortInfo<M>> getSortInfo() { return comparators; } /** * Returns true if the two models have the same key. * * @param model1 the first model * @param model2 the second model * @return true if equals */ public boolean hasMatchingKey(M model1, M model2) { return keyProvider.getKey(model1).equals(keyProvider.getKey(model2)); } /** * Returns true if a record exists for the given model. * * @param data the model * @return true if a record exists */ public boolean hasRecord(M data) { return records.get(getKeyProvider().getKey(data)) != null; } /** * Returns true if auto commit is enabled. * * @return true if auto commit is enabled */ public boolean isAutoCommit() { return isAutoCommit; } /** * Returns true if filtering is enabled, whether or not filters are present. * * @return true if filtering is enabled */ public boolean isEnableFilters() { return filtersEnabled; } /** * Returns true if filtering is enabled AND the store has filters. * * @return true if the store is filtered */ public boolean isFiltered() { return filtersEnabled && filters != null && filters.size() != 0; } /** * Cancel outstanding changes on all changed records. */ public void rejectChanges() { for (Record r : modifiedRecords) { r.revert(); } modifiedRecords.clear(); } /** * Removes the filter from the store's set of filters. Runs the filters again * if they are enabled, and the filter was actually in the list. * * @param filter the filter to be removed */ public void removeFilter(StoreFilter<M> filter) { if (filters != null) { if (filters.remove(filter) && filtersEnabled) { // the list of active filters has changed, unless we cache what showed // what item, we need to re-run the whole set applyFilters(); } } } /** * Removes all filters. */ public void removeFilters() { if (filters != null) { filters.clear(); applyFilters(); } } /** * Enables or disables auto commit. If auto commit is enabled, the change will * be made directly to the model, else the change will be queued up until * commit() is called. * * @param isAutoCommit true to enable auto commit * @see Record#addChange(ValueProvider, Object) */ public void setAutoCommit(boolean isAutoCommit) { this.isAutoCommit = isAutoCommit; } /** * Enables or disables the filters. * * @param enableFilters true to enable filters */ public void setEnableFilters(boolean enableFilters) { if (this.filtersEnabled == enableFilters) { return; } this.filtersEnabled = enableFilters; applyFilters(); } /** * Replaces the item that matches the key of the given item, and fires a {@link StoreUpdateEvent} to indicate that * this change has occurred. Any changes to the previous model via it's record instance will be lost and the record * will be removed. * * This will not cause the sort or filter to be re-applied to the object. * * @param item the new item to take its place in the Store. */ public abstract void update(M item); /** * Sets the filters to run again, whether they need it or not. Will fire a * Filter event if anything has changed. */ protected abstract void applyFilters(); /** * Creates a new master <code>Comparator</code> that runs all the * {@link StoreSortInfo} comparators in sequence, returning the value of the * first comparator that returns a non-zero value, or zero if no comparator * returns a non-zero value. * * @return the new master comparator */ protected Comparator<M> buildFullComparator() { return new Comparator<M>() { public int compare(M o1, M o2) { for (int i = 0; i < comparators.size(); i++) { int val = comparators.get(i).compare(o1, o2); if (val != 0) { return val; } } return 0; } }; } /** * Removes all items from the store. */ protected void clear() { modifiedRecords.clear(); records.clear(); } /** * Ensures the store's handler manager exists, creating it if necessary (lazy * construction). * * @return the store's handler manager */ protected HandlerManager ensureHandlers() { if (handlerManager == null) { handlerManager = new HandlerManager(this); } return handlerManager; } /** * Returns true if the store is sorted ({@link StoreSortInfo} has been added * and not cleared). * * @return true if the store is sorted. */ protected boolean isSorted() { return comparators.size() != 0; } /** * Cleans up any reference the Store might've had to the model. Must be called * by subclasses. Returns a boolean, as {@link Collection#remove(Object)} * * @param model the data model to remove * @return boolean, indicating if it was removed from the Store. Subclasses * should modify this to return false if necessary */ protected boolean remove(M model) { String key = getKeyProvider().getKey(model); if (records.containsKey(key)) { modifiedRecords.remove(records.remove(key)); } return true; } /** * Returns a ValueProvider that will read and write to the corresponding Record object if * necessary instead of the model itself. This allows sorting and filtering to cover the * new value in the record instead of the original value. * @param valueProvider an existing ValueProvider to wrap * @return a new ValueProvider of the same type, able to read and write from this store's records */ public <V> ValueProvider<? super M, V> wrapRecordValueProvider( final ValueProvider<? super M, V> valueProvider) { if (isAutoCommit()) { return valueProvider; } return new ValueProvider<M, V>() { @Override public V getValue(M object) { if (hasRecord(object)) { return getRecord(object).getValue(valueProvider); } return valueProvider.getValue(object); } @Override public void setValue(M object, V value) { getRecord(object).addChange(valueProvider, value); } @Override public String getPath() { return valueProvider.getPath(); } }; } }