com.sencha.gxt.data.shared.Store.java Source code

Java tutorial

Introduction

Here is the source code for com.sencha.gxt.data.shared.Store.java

Source

/**
 * 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();
            }
        };
    }
}