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