/* * Copyright 2005 Joe Walker * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.directwebremoting.datasync; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; 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.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<T> extends AbstractStoreProvider<T> implements StoreProvider<T> { /** * Initialize the MapStoreProvider from an existing map + specified type. */ public MapStoreProvider(Map<String, T> datamap, Class<T> type) { this(datamap, type, new ArrayList<SortCriterion>(), new DefaultComparatorFactory<T>()); } /** * Initialize the MapStoreProvider from an existing map + specified type. */ public MapStoreProvider(Map<String, T> datamap, Class<T> type, ComparatorFactory<T> comparatorFactory) { this(datamap, type, new ArrayList<SortCriterion>(), comparatorFactory); } /** * Initialize an empty MapStoreProvider from the specified type. */ public MapStoreProvider(Class<T> type) { this(new HashMap<String, T>(), type, new ArrayList<SortCriterion>(), new DefaultComparatorFactory<T>()); } /** * Initialize the MapStoreProvider from an existing map + specified type * along with some sort criteria to be used when the client does not specify * sorting. */ public MapStoreProvider(Map<String, T> map, Class<T> type, List<SortCriterion> defaultCriteria, ComparatorFactory<T> comparatorFactory) { super(type); this.baseRegion = new StoreRegion(0, -1, defaultCriteria, null); this.comparatorFactory = comparatorFactory; Index index = new Index(baseRegion, map); data.put(baseRegion, index); } /* (non-Javadoc) * @see org.directwebremoting.datasync.StoreProvider#viewRegion(org.directwebremoting.datasync.StoreRegion) */ public synchronized MatchedItems viewRegion(StoreRegion region) { Index index = getIndex(region); return selectMatchedItems(index.getSortedData(), region.getStart(), region.getCount()); } /* (non-Javadoc) * @see org.directwebremoting.datasync.StoreProvider#viewRegion(org.directwebremoting.datasync.StoreRegion, org.directwebremoting.datasync.StoreChangeListener) */ public synchronized MatchedItems viewRegion(StoreRegion region, StoreChangeListener<T> listener) { MatchedItems matchedItems = viewRegion(region); Collection<String> itemIds = new HashSet<String>(); for (Item item : matchedItems.getViewedMatches()) { itemIds.add(item.getItemId()); } setWatchedSet(listener, itemIds); return matchedItems; } /* (non-Javadoc) * @see org.directwebremoting.datasync.StoreProvider#viewItem(java.lang.String, org.directwebremoting.io.StoreChangeListener) */ public Item viewItem(String itemId, StoreChangeListener<T> listener) { Item item = viewItem(itemId); if (item != null) { addWatchedSet(listener, Arrays.asList(item.getItemId())); } return item; } /* (non-Javadoc) * @see org.directwebremoting.datasync.StoreProvider#unsubscribe(org.directwebremoting.datasync.StoreChangeListener) */ public synchronized void unsubscribe(StoreChangeListener<T> listener) { setWatchedSet(listener, null); } /* (non-Javadoc) * @see org.directwebremoting.datasync.StoreProvider#put(java.lang.String, java.lang.Object) */ public synchronized void put(String itemId, T value) { for (Index index : data.values()) { index.put(itemId, value, true); } } /* (non-Javadoc) * @see org.directwebremoting.datasync.StoreProvider#update(java.util.List) */ public synchronized void update(List<ItemUpdate> changes) throws SecurityException { // First off group the changes by ID so we can fire updates together Map<String, List<ItemUpdate>> groupedChanges = new HashMap<String, List<ItemUpdate>>(); for (ItemUpdate itemUpdate : changes) { List<ItemUpdate> itemChanges = groupedChanges.get(itemUpdate.getItemId()); if (itemChanges == null) { itemChanges = new ArrayList<ItemUpdate>(); groupedChanges.put(itemUpdate.getItemId(), itemChanges); } itemChanges.add(itemUpdate); } // Make the changes to each item in one go for (Map.Entry<String, List<ItemUpdate>> entry : groupedChanges.entrySet()) { T t = getObject(entry.getKey()); Collection<String> changedAttributes = new HashSet<String>(); for (ItemUpdate itemUpdate : changes) { String attribute = itemUpdate.getAttribute(); Class<?> convertTo = LocalUtil.getPropertyType(t.getClass(), attribute); Object value = convert(itemUpdate.getNewValue(), convertTo); try { LocalUtil.setProperty(t, attribute, value); changedAttributes.add(attribute); } catch (SecurityException ex) { throw ex; } catch (Exception ex) { throw new SecurityException(ex); } } Item item = new Item(entry.getKey(), t); fireItemChanged(item, changedAttributes); } } /* (non-Javadoc) * @see org.directwebremoting.datasync.AbstractStoreProvider#getObject(java.lang.String) */ @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<String, T> asMap() { final Index original = data.get(baseRegion); return new AbstractMap<String, T>() { /* (non-Javadoc) * @see java.util.AbstractMap#put(K, V) */ @Override public T put(String itemId, T value) { T old = getObject(itemId); MapStoreProvider.this.put(itemId, value); return old; } /* (non-Javadoc) * @see java.util.AbstractMap#remove(java.lang.Object) */ @Override public T remove(Object itemId) { T old = MapStoreProvider.this.getObject((String) itemId); MapStoreProvider.this.put((String) itemId, (T) null); return old; } /* (non-Javadoc) * @see java.util.AbstractMap#entrySet() */ @Override public Set<Entry<String, T>> entrySet() { return new AbstractSet<Entry<String, T>>() { /* (non-Javadoc) * @see java.util.AbstractCollection#iterator() */ @Override public Iterator<Entry<String, T>> 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<String, T> 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<String, T> entry = (Entry<String, T>) 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; } if (region.getSort() == null) { // we need to change to the default sorting region = new StoreRegion(region.getStart(), region.getCount(), baseRegion.getSort(), region.getQuery()); } 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<String, T> map) { sort = baseRegion.getSort(); query = baseRegion.getQuery(); sortedData = createEmptySortedData(); for (Map.Entry<String, T> 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(); sortedData = createEmptySortedData(); for (Pair<String, T> pair : original.sortedData) { put(pair, false); } } /** * For use only by the constructor. Sets up the comparators. */ private SortedSet<Pair<String, T>> createEmptySortedData() { // This is really how we sort - according to the defaultSearchCriteria Comparator<T> criteriaComparator = new SortCriteriaComparator<T>(sort, comparatorFactory); // However we need to store a the data along with a key so we need a // proxy comparator to be a Comparator<Pair<T, String>> but to call // the real comparator above. Comparator<Pair<String, T>> pairComparator = new PairComparator<T>(criteriaComparator); // Copy all the data from the original map into pairs in a sorted set return new TreeSet<Pair<String, T>>(pairComparator); } /** * Accessor for the sorted data */ public SortedSet<Pair<String, T>> getSortedData() { return sortedData; } /** * Remove an item from this cache of data */ public void remove(String itemId) { T t = index.remove(itemId); sortedData.remove(new Pair<String, T>(itemId, t)); fireItemRemoved(itemId); } /** * Add an item thats already in a pair */ public void put(Pair<String, T> 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 */ 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<String, T>(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); } /* (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 + "]"; } /** * The data sorted by object according to our sort criteria */ private final SortedSet<Pair<String, T>> sortedData; /** * The data in a standard hash so we can lookup by itemId */ private final Map<String, T> index = new HashMap<String, T>(); /** * The criteria by which we are sorting */ private final List<SortCriterion> sort; /** * The way we are filtering the data */ private final Map<String, String> query; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @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<T> comparatorFactory; /** * There will always be at least one entry in the {@link #data} map with * this key. * @protectedBy(this) */ protected final StoreRegion baseRegion; /** * We actually store a number of indexes to the real data. * @protectedBy(this) */ protected final Map<StoreRegion, Index> data = new HashMap<StoreRegion, Index>(); /** * The log stream */ private static final Log log = LogFactory.getLog(MapStoreProvider.class); }