All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.directwebremoting.datasync.MapStoreProvider Maven / Gradle / Ivy

package org.directwebremoting.datasync;

import java.util.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.directwebremoting.io.Item;
import org.directwebremoting.io.ItemUpdate;
import org.directwebremoting.io.MatchedItems;
import org.directwebremoting.io.QueryOptions;
import org.directwebremoting.io.SortCriterion;
import org.directwebremoting.io.StoreChangeListener;
import org.directwebremoting.io.StoreRegion;
import org.directwebremoting.util.LocalUtil;
import org.directwebremoting.util.Pair;

/**
 * A simple implementation of StoreProvider that uses a Map<String, ?>.
 * @author Joe Walker [joe at getahead dot ltd dot uk]
 */
public class MapStoreProvider extends AbstractStoreProvider implements StoreProvider
{
    /**
     * Initialize the MapStoreProvider from an existing map + specified type.
     * @param datamap ...
     * @param type ...
     */
    public MapStoreProvider(Map datamap, Class type)
    {
        this(datamap, type, new ArrayList(), new DefaultComparatorFactory());
    }

    /**
     * Initialize the MapStoreProvider from an existing map + specified type.
     * @param datamap ...
     * @param type ...
     * @param comparatorFactory ...
     */
    public MapStoreProvider(Map datamap, Class type, ComparatorFactory comparatorFactory)
    {
        this(datamap, type, new ArrayList(), comparatorFactory);
    }

    /**
     * Initialize an empty MapStoreProvider from the specified type.
     * @param type ...
     */
    public MapStoreProvider(Class type)
    {
        this(new HashMap(), type, new ArrayList(), new DefaultComparatorFactory());
    }

    /**
     * Initialize the MapStoreProvider from an existing map + specified type
     * along with some sort criteria to be used when the client does not specify
     * sorting.
     * @param map ...
     * @param type ...
     * @param defaultCriteria ...
     * @param comparatorFactory ...
     * */
    public MapStoreProvider(Map map, Class type, List defaultCriteria, ComparatorFactory comparatorFactory)
    {
        super(type);
        this.baseRegion = new StoreRegion(0, -1, defaultCriteria, null, new QueryOptions());
        this.comparatorFactory = comparatorFactory;

        Index index = new Index(baseRegion, map);
        data.put(baseRegion, index);
    }

    public synchronized MatchedItems viewRegion(StoreRegion region)
    {
        Index index = getIndex(region);
        return selectMatchedItems(index.getSortedData(), region.getStart(), region.getCount());
    }

    public synchronized MatchedItems viewRegion(StoreRegion region, StoreChangeListener listener)
    {
        MatchedItems matchedItems = viewRegion(region);

        Collection itemIds = new HashSet();
        for (Item item : matchedItems.getViewedMatches())
        {
            itemIds.add(item.getItemId());
        }
        setWatchedSet(listener, itemIds);

        return matchedItems;
    }

    public Item viewItem(String itemId, StoreChangeListener listener)
    {
        Item item = viewItem(itemId);

        if (item != null)
        {
            addWatchedSet(listener, Collections.singletonList(item.getItemId()));
        }

        return item;
    }

    public synchronized void unsubscribe(StoreChangeListener listener)
    {
        setWatchedSet(listener, null);
    }

    public synchronized void put(String itemId, T value)
    {
        put(itemId, value, true);
    }

    public synchronized void put(String itemId, T value, boolean notify)
    {
        for (Index index : data.values())
        {
            index.put(itemId, value, notify);
        }
    }

    public synchronized void update(List changes) throws SecurityException
    {
        // First off group the changes by ID so we can fire updates together
        Map> groupedChanges = new HashMap>();
        for (ItemUpdate itemUpdate : changes)
        {
            List itemChanges = groupedChanges.get(itemUpdate.getItemId());
            if (itemChanges == null)
            {
                itemChanges = new ArrayList();
                groupedChanges.put(itemUpdate.getItemId(), itemChanges);
            }
            itemChanges.add(itemUpdate);
        }

        // Make the changes to each item in one go
        for (Map.Entry> entry : groupedChanges.entrySet())
        {
            T t = getObject(entry.getKey());
            boolean newItem = t == null;
            Collection changedAttributes = new HashSet();
            if (newItem)
            {
                try
                {
                    t = type.getConstructor().newInstance();
                }
                catch (Exception ex)
                {
                    throw new SecurityException(ex);
                }
            }

            for (ItemUpdate itemUpdate : entry.getValue())
            {
                String attribute = itemUpdate.getAttribute();
                if (attribute.equals("$delete"))
                {
                    put(itemUpdate.getItemId(), (T) null);
                }
                else
                {
                    try
                    {
                        Class convertTo = LocalUtil.getPropertyType(type, attribute);
                        Object value = convert(itemUpdate.getNewValue(), convertTo);
                        LocalUtil.setProperty(t, attribute, value);
                        changedAttributes.add(attribute);
                    }
                    catch (SecurityException ex)
                    {
                        throw ex;
                    }
                    catch (Exception ex)
                    {
                        throw new SecurityException(ex);
                    }
                }
            }

            if (!changedAttributes.isEmpty())
            {
                Item item = new Item(entry.getKey(), t);
                if (newItem)
                {
                    put(entry.getKey(), t, false);
                    fireItemAdded(item);
                }
                else
                {
                    fireItemChanged(item, changedAttributes);
                }
            }
        }
    }

    @Override protected synchronized T getObject(String itemId)
    {
        return data.get(baseRegion).index.get(itemId);
    }

    /**
     * Get access to the contained data as a {@link Map}.
     * @return An implementation of Map that affects this {@link StoreProvider}
     */
    public synchronized Map asMap()
    {
        final Index original = data.get(baseRegion);

        return new AbstractMap() {
            @Override public T put(String itemId, T value)
            {
                T old = getObject(itemId);
                MapStoreProvider.this.put(itemId, value);
                return old;
            }

            @Override public T remove(Object itemId)
            {
                T old = MapStoreProvider.this.getObject((String) itemId);
                MapStoreProvider.this.put((String) itemId, (T) null);
                return old;
            }

            @Override public Set> entrySet()
            {
                return new AbstractSet>()
                {
                    /* (non-Javadoc)
                     * @see java.util.AbstractCollection#iterator()
                     */
                    @Override
                    public Iterator> iterator()
                    {
                        return original.index.entrySet().iterator();
                    }

                    /* (non-Javadoc)
                     * @see java.util.AbstractCollection#size()
                     */
                    @Override
                    public int size()
                    {
                        return original.sortedData.size();
                    }

                    /* (non-Javadoc)
                     * @see java.util.AbstractCollection#add(java.lang.Object)
                     */
                    @Override
                    public boolean add(Entry entry)
                    {
                        T t = getObject(entry.getKey());
                        MapStoreProvider.this.put(entry.getKey(), entry.getValue());
                        return t != null;
                    }

                    /* (non-Javadoc)
                     * @see java.util.AbstractCollection#remove(java.lang.Object)
                     */
                    @Override
                    public boolean remove(Object o)
                    {
                        @SuppressWarnings("unchecked")
                        Entry entry = (Entry) o;
                        T old = getObject(entry.getKey());
                        MapStoreProvider.this.put(entry.getKey(), (T) null);
                        return old != null;
                    }
                };
            }
        };
    }

    /**
     * Get an Index from a StoreRegion by defaulting the sort criteria if
     * needed, and by creating a new index if needed.
     * @param region The region to be viewed (we ignore start/end)
     * @return An index that we can use to get a sorted data cache
     */
    protected synchronized Index getIndex(StoreRegion region)
    {
        if (region == null)
        {
            region = baseRegion;
        }

        Index index = data.get(region);

        if (index == null)
        {
            // So there is no index that looks like we want
            Index original = data.get(baseRegion);

            index = new Index(region, original);
            data.put(region, index);

            log.debug("Creating new Index: " + index);
        }
        else
        {
            log.debug("Using existing Index: " + index);
        }

        return index;
    }

    /**
     * An Index represents the data in a {@link MapStoreProvider} sorted
     * according to a certain {@link #sort} and {@link #query}
     */
    protected class Index
    {
        /**
         * This constructor should only be used from the constructor of our
         * parent MapStoreProvider.
         * @param baseRegion The portion of the data that we are looking at
         * @param map The data to filter and copy for this baseRegion
         */
        public Index(StoreRegion baseRegion, Map map)
        {
            sort = baseRegion.getSort();
            query = baseRegion.getQuery();
            sortedData = createEmptySortedData();
            options = baseRegion.getQueryOptions();

            for (Map.Entry entry : map.entrySet())
            {
                put(entry.getKey(), entry.getValue(), false);
            }
        }

        /**
         * Constructor for use to copy data from an existing Index
         * @param region The portion of the data that we are looking at
         * @param original The data to filter and copy for this baseRegion
         */
        public Index(StoreRegion region, Index original)
        {
            sort = region.getSort();
            query = region.getQuery();
            options = region.getQueryOptions();
            sortedData = createEmptySortedData();

            for (Pair pair : original.sortedData)
            {
                put(pair, false);
            }
        }

        /**
         * For use only by the constructor. Sets up the comparators.
         */
        private SortedSet> createEmptySortedData()
        {
            // This is really how we sort - according to the defaultSearchCriteria
            Comparator criteriaComparator = new SortCriteriaComparator(sort, comparatorFactory);

            // However we need to store a the data along with a key so we need a
            // proxy comparator to be a Comparator> but to call
            // the real comparator above.
            Comparator> pairComparator = new PairComparator(criteriaComparator);

            // Copy all the data from the original map into pairs in a sorted set
            return new TreeSet>(pairComparator);
        }

        /**
         * Accessor for the sorted data
         * @return ...
         */
        public SortedSet> getSortedData()
        {
            return sortedData;
        }

        /**
         * Remove an item from this cache of data
         * @param itemId ...
         */
        public void remove(String itemId)
        {
            T t = index.remove(itemId);
            sortedData.remove(new Pair(itemId, t));
            fireItemRemoved(itemId);
        }

        /**
         * Add an item that's already in a pair
         * @param pair ...
         * @param notify ...
         */
        public void put(Pair pair, boolean notify)
        {
            if (pair.right == null)
            {
                remove(pair.left);
                return;
            }

            if (isRelevant(pair.right))
            {
                boolean existing = index.containsKey(pair.left);
                sortedData.add(pair);
                index.put(pair.left, pair.right);

                if (notify)
                {
                    if (existing)
                    {
                        fireItemChanged(new Item(pair.left, pair.right), null);
                    }
                    else
                    {
                        fireItemAdded(new Item(pair.left, pair.right));
                    }
                }
            }
        }

        /**
         * Add an entry by separate objects
         * @param itemId ...
         * @param t ...
         * @param notify ...
         */
        public void put(String itemId, T t, boolean notify)
        {
            if (t == null)
            {
                remove(itemId);
                return;
            }

            if (isRelevant(t))
            {
                boolean existing = index.containsKey(itemId);
                sortedData.add(new Pair(itemId, t));
                index.put(itemId, t);

                if (notify)
                {
                    if (existing)
                    {
                        fireItemChanged(new Item(itemId, t), null);
                    }
                    else
                    {
                        fireItemAdded(new Item(itemId, t));
                    }
                }
            }
        }

        /**
         * Is this item one that will appear in this Index?
         */
        private boolean isRelevant(T t)
        {
            return query == null || query.isEmpty() || passesFilter(t, query, options);
        }

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString()
        {
            return "Map.Index[sortedData.size=" + sortedData.size() + ",index.size=" + index.size() + ",sort=" + sort + ",query=" + query + ", options=" + options + "]";
        }

        @Override
        public boolean equals(Object obj)
        {
            if (obj == null)
            {
                return false;
            }

            if (obj == this)
            {
                return true;
            }

            if (!this.getClass().equals(obj.getClass()))
            {
                return false;
            }

            @SuppressWarnings("unchecked") Index that = (Index) obj;
            if (!LocalUtil.equals(this.options, that.options)) {
                return false;
            }

            if (!LocalUtil.equals(this.query, that.query)) {
                return false;
            }

            return LocalUtil.equals(this.sort, that.sort);
        }

        @Override
        public int hashCode()
        {
            int hash = 99211;
            hash += sort != null ? sort.hashCode() : 42835;
            hash += query != null ? query.hashCode() : 52339;
            hash += options != null ? options.hashCode() : 39832;
            return hash;
        }

        /**
         * The data sorted by object according to our sort criteria
         */
        private final SortedSet> sortedData;

        /**
         * The data in a standard hash so we can lookup by itemId
         */
        private final Map index = new HashMap();

        /**
         * The criteria by which we are sorting
         */
        private final List sort;

        /**
         * The way we are filtering the data
         */
        private final Map query;

        /**
         * Criteria to filter the query matches.
         */
        private final QueryOptions options;

    }

    @Override public synchronized String toString() {
        Index original = data.get(baseRegion);
        return "MapStoreProvider[type=" + type.getSimpleName() + ",entries=" + original.index.size() + ",indexes=" + data.size() + "]";
    }

    /**
     * How we find Comparators to compare Ts based on a given attribute
     */
    protected final ComparatorFactory comparatorFactory;

    /**
     * There will always be at least one entry in the {@link #data} map with
     * this key.
     */
    protected final StoreRegion baseRegion;

    /**
     * We actually store a number of indexes to the real data.
     */
    protected final Map data = new HashMap();

    /**
     * The log stream
     */
    private static final Log log = LogFactory.getLog(MapStoreProvider.class);
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy